feat: align completed order detail modal with H5
This commit is contained in:
parent
3a42f65c5a
commit
b31c68dcfc
|
|
@ -6,6 +6,8 @@ export const MOBILE_ASSET_BASE = `${MAP_ASSET_BASE}/images-mobile`
|
||||||
export const BANNER_BG = `${MOBILE_ASSET_BASE}/bg-banner.png`
|
export const BANNER_BG = `${MOBILE_ASSET_BASE}/bg-banner.png`
|
||||||
export const CARD_HEADER = `${MOBILE_ASSET_BASE}/card_header.png`
|
export const CARD_HEADER = `${MOBILE_ASSET_BASE}/card_header.png`
|
||||||
export const PAGE_TITLE = '网上快办平台'
|
export const PAGE_TITLE = '网上快办平台'
|
||||||
|
export const RECEIPT_STAMP_DONE = `${STATIC_CDN}/images/oderDetailCallBackSuccess.png`
|
||||||
|
export const RECEIPT_STAMP_PENDING = `${STATIC_CDN}/images/oderDetailCallBack.png`
|
||||||
|
|
||||||
export const ENTITY_LABELS = {
|
export const ENTITY_LABELS = {
|
||||||
entityName: '门店名称',
|
entityName: '门店名称',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { getReceiptMeta, numberToUpperCase, pickGoodsType, shouldShowReceipt, splitOrderFiles } from './detail'
|
||||||
|
|
||||||
|
describe('splitOrderFiles', () => {
|
||||||
|
it('separates screenshot images from pdf reports', () => {
|
||||||
|
expect(splitOrderFiles([
|
||||||
|
{ id: '1', url: 'https://cdn.example/a.jpg' },
|
||||||
|
{ id: '2', url: 'http://cdn.example/report.pdf' },
|
||||||
|
{ id: '3', url: 'https://cdn.example/b.png?ver=1' },
|
||||||
|
{ id: '4', url: 'https://cdn.example/note.PDF' },
|
||||||
|
])).toEqual({
|
||||||
|
images: [
|
||||||
|
{ id: '1', url: 'https://cdn.example/a.jpg' },
|
||||||
|
{ id: '3', url: 'https://cdn.example/b.png?ver=1' },
|
||||||
|
],
|
||||||
|
pdfs: [
|
||||||
|
{ id: '2', url: 'https://cdn.example/report.pdf' },
|
||||||
|
{ id: '4', url: 'https://cdn.example/note.PDF' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty groups when files are missing', () => {
|
||||||
|
expect(splitOrderFiles()).toEqual({ images: [], pdfs: [] })
|
||||||
|
expect(splitOrderFiles([])).toEqual({ images: [], pdfs: [] })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shouldShowReceipt', () => {
|
||||||
|
it('shows receipt for corp goods types and hides it for map_label', () => {
|
||||||
|
expect(shouldShowReceipt('license_year')).toBe(true)
|
||||||
|
expect(shouldShowReceipt('license_destory')).toBe(true)
|
||||||
|
expect(shouldShowReceipt('credit_repair')).toBe(true)
|
||||||
|
expect(shouldShowReceipt('map_label')).toBe(false)
|
||||||
|
expect(shouldShowReceipt('unknown')).toBe(false)
|
||||||
|
expect(shouldShowReceipt()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getReceiptMeta', () => {
|
||||||
|
it('returns title and remark for each corp goods type', () => {
|
||||||
|
expect(getReceiptMeta('license_year').title).toBe('信息申报回执单')
|
||||||
|
expect(getReceiptMeta('license_year').description).toContain('年报国家公示官网')
|
||||||
|
expect(getReceiptMeta('license_destory').title).toBe('工商信息注销回执单')
|
||||||
|
expect(getReceiptMeta('license_destory').description).toContain('登记状态')
|
||||||
|
expect(getReceiptMeta('credit_repair').title).toBe('工商信息修复回执单')
|
||||||
|
expect(getReceiptMeta('credit_repair').description).toContain('creditchina.gov.cn')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to goods name when type is unknown', () => {
|
||||||
|
expect(getReceiptMeta('unknown', '自定义业务')).toEqual({
|
||||||
|
title: '自定义业务回执单',
|
||||||
|
description: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('numberToUpperCase', () => {
|
||||||
|
it('converts receipt amounts the same way as H5', () => {
|
||||||
|
expect(numberToUpperCase('')).toBe('')
|
||||||
|
expect(numberToUpperCase('0')).toBe('零元整')
|
||||||
|
expect(numberToUpperCase('1')).toBe('壹元整')
|
||||||
|
expect(numberToUpperCase('10')).toBe('壹拾元整')
|
||||||
|
expect(numberToUpperCase('100.5')).toBe('壹佰元伍角')
|
||||||
|
expect(numberToUpperCase('123.45')).toBe('壹佰贰拾叁元肆角伍分')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('pickGoodsType', () => {
|
||||||
|
it('prefers prev_info goods_type then first goods item', () => {
|
||||||
|
expect(pickGoodsType({
|
||||||
|
data: {
|
||||||
|
prev_info: { goods_type: 'license_year' },
|
||||||
|
goods: [{ goods_type: 'map_label' }],
|
||||||
|
},
|
||||||
|
})).toBe('license_year')
|
||||||
|
expect(pickGoodsType({
|
||||||
|
prevInfo: { type: 'credit_repair' },
|
||||||
|
})).toBe('credit_repair')
|
||||||
|
expect(pickGoodsType({
|
||||||
|
data: { goods: [{ goods_type: 'map_label' }] },
|
||||||
|
})).toBe('map_label')
|
||||||
|
expect(pickGoodsType({})).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
export const CORP_GOODS_TYPES = ['license_year', 'license_destory', 'credit_repair'] as const
|
||||||
|
|
||||||
|
export type CorpGoodsType = (typeof CORP_GOODS_TYPES)[number]
|
||||||
|
|
||||||
|
const RECEIPT_META: Record<CorpGoodsType, { title: string, description: string }> = {
|
||||||
|
credit_repair: {
|
||||||
|
title: '工商信息修复回执单',
|
||||||
|
description:
|
||||||
|
'信用网站官网 https://www.creditchina.gov.cn/\n' +
|
||||||
|
'搜索企业: 在首页搜索框输入公司全称或统一社会信用代码进行查询\n' +
|
||||||
|
'查看信用信息: 进入企业信用信息详情页;\n' +
|
||||||
|
'重点关注:\n' +
|
||||||
|
'1."行政处罚"栏目: 查看该条被修复的行政处罚信息状态。如果修复成功,通常会显示"已修复"、"已信用修复"、"已撤下"等状态标识,或者该条信息已不再显示在公示列表中;\n' +
|
||||||
|
'2."信用修复"栏目(如果有): 有些平台会单独列出企业的信用修复申请记录及状态(如:申请中、审核通过/修复成功、审核不通过);\n' +
|
||||||
|
'3.查看"失信信息"或"重点关注名单": 如果该处罚曾导致企业被列入失信名单或重点关注名单,确认这些名单中是否已移除该公司;',
|
||||||
|
},
|
||||||
|
license_destory: {
|
||||||
|
title: '工商信息注销回执单',
|
||||||
|
description:
|
||||||
|
'访问官网(http://www.gsxt.gov.cn),输入企业名称或统一社会信用代码进行搜索。\n' +
|
||||||
|
'在企业详情页面查看“登记状态”栏是否标注为“注销”\n' +
|
||||||
|
'注意:注销信息通常需1-3天完成公示更新,建议完成注销流程后次日查询。',
|
||||||
|
},
|
||||||
|
license_year: {
|
||||||
|
title: '信息申报回执单',
|
||||||
|
description:
|
||||||
|
'年报国家公示官网:https://www.gsxt.gov.cn\n' +
|
||||||
|
'1.年报公示日期为7个工作日,到期后可自行登陆国家企业信用信息官网查询;\n' +
|
||||||
|
'2.您的个人信息(姓名、电话、身份证),只用于您办理业务时使用;\n' +
|
||||||
|
'3.企业一站式服务平台为第三方企业服务公司,办理业务方便快捷,办理不成功退全部费用;',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCorpGoodsType (goodsType?: string): goodsType is CorpGoodsType {
|
||||||
|
return (CORP_GOODS_TYPES as readonly string[]).includes(goodsType || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowReceipt (goodsType?: string) {
|
||||||
|
return isCorpGoodsType(goodsType)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickGoodsType (payload: any): string {
|
||||||
|
const data = payload?.data ?? payload ?? {}
|
||||||
|
const prev = data.prev_info ?? data.prevInfo ?? payload?.prevInfo ?? {}
|
||||||
|
const fromPrev = String(prev.goods_type || prev.type || '').trim()
|
||||||
|
if (fromPrev) return fromPrev
|
||||||
|
|
||||||
|
const goods = Array.isArray(data.goods) ? data.goods : []
|
||||||
|
const first = goods[0] || {}
|
||||||
|
return String(first.goods_type || first.type || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReceiptMeta (goodsType?: string, goodsName?: string) {
|
||||||
|
if (isCorpGoodsType(goodsType)) {
|
||||||
|
return RECEIPT_META[goodsType]
|
||||||
|
}
|
||||||
|
const name = String(goodsName || '').trim()
|
||||||
|
return {
|
||||||
|
title: name ? `${name}回执单` : '回执单',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderFile = {
|
||||||
|
id?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPdfUrl (url: string) {
|
||||||
|
return /\.pdf(?:$|[?#])/i.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitOrderFiles (files?: OrderFile[] | null) {
|
||||||
|
const images: OrderFile[] = []
|
||||||
|
const pdfs: OrderFile[] = []
|
||||||
|
|
||||||
|
for (const file of files || []) {
|
||||||
|
const url = typeof file?.url === 'string' ? file.url.replace(/^http:\/\//i, 'https://') : ''
|
||||||
|
if (!url) continue
|
||||||
|
const nextFile = url === file.url ? file : { ...file, url }
|
||||||
|
if (isPdfUrl(url)) pdfs.push(nextFile)
|
||||||
|
else images.push(nextFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { images, pdfs }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function numberToUpperCase (money: unknown): string {
|
||||||
|
const cnNums = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']
|
||||||
|
const cnIntRadice = ['', '拾', '佰', '仟']
|
||||||
|
const cnIntUnits = ['', '万', '亿', '兆']
|
||||||
|
const cnDecUnits = ['角', '分']
|
||||||
|
const cnInteger = '整'
|
||||||
|
const cnIntLast = '元'
|
||||||
|
|
||||||
|
if (money === '' || money === undefined || money === null) return ''
|
||||||
|
|
||||||
|
const text = String(money)
|
||||||
|
const cashText = text.charAt(0) === '-' ? text.slice(1) : text
|
||||||
|
let cash = parseFloat(cashText)
|
||||||
|
if (cash === 0) return `${cnNums[0]}${cnIntLast}${cnInteger}`
|
||||||
|
if (!Number.isFinite(cash)) return ''
|
||||||
|
|
||||||
|
const [integerNum, rawDecimal = ''] = cash.toString().split('.')
|
||||||
|
const decimalNum = rawDecimal.substr(0, 2)
|
||||||
|
let chineseStr = ''
|
||||||
|
|
||||||
|
if (integerNum !== '-' && parseInt(integerNum, 10) > 0) {
|
||||||
|
const intLen = integerNum.length
|
||||||
|
let zero = 0
|
||||||
|
for (let i = 0; i < intLen; i++) {
|
||||||
|
const intChar = integerNum.substr(i, 1)
|
||||||
|
const intSlen = intLen - i - 1
|
||||||
|
const divided = intSlen / 4
|
||||||
|
const remain = intSlen % 4
|
||||||
|
|
||||||
|
if (intChar === '0') {
|
||||||
|
zero += 1
|
||||||
|
} else {
|
||||||
|
if (zero > 0) chineseStr += cnNums[0]
|
||||||
|
zero = 0
|
||||||
|
chineseStr += cnNums[parseInt(intChar, 10)] + cnIntRadice[remain]
|
||||||
|
}
|
||||||
|
if (remain === 0 && divided > 0) {
|
||||||
|
chineseStr += cnIntUnits[divided]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chineseStr += cnIntLast
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decimalNum) {
|
||||||
|
for (let i = 0; i < decimalNum.length; i++) {
|
||||||
|
const decChar = decimalNum.substr(i, 1)
|
||||||
|
if (decChar !== '0') {
|
||||||
|
chineseStr += cnNums[parseInt(decChar, 10)] + cnDecUnits[i]
|
||||||
|
}
|
||||||
|
if (decChar === '0' && parseInt(integerNum, 10) > 0) {
|
||||||
|
chineseStr += cnNums[0] + cnDecUnits[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chineseStr += cnInteger
|
||||||
|
}
|
||||||
|
|
||||||
|
return chineseStr
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
buildResubmitPayload,
|
buildResubmitPayload,
|
||||||
filterMapLabelOrders,
|
filterMapLabelOrders,
|
||||||
|
filterOrdersForProduct,
|
||||||
formatMapCoordinate,
|
formatMapCoordinate,
|
||||||
getOrderAddressName,
|
getOrderAddressName,
|
||||||
getOrderExtraImages,
|
getOrderExtraImages,
|
||||||
|
|
@ -40,6 +41,31 @@ describe('filterMapLabelOrders', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('filterOrdersForProduct', () => {
|
||||||
|
it('keeps every order when querying from a corp goods type', () => {
|
||||||
|
const orders = filterOrdersForProduct('license_year', [
|
||||||
|
mapOrder,
|
||||||
|
{ ...mapOrder, order_id: '5', goods_type: 'license_year' },
|
||||||
|
] as any)
|
||||||
|
|
||||||
|
expect(orders.map((item) => item.order_id)).toEqual(['4', '5'])
|
||||||
|
expect(orders[0].extra.goods_param).toEqual({
|
||||||
|
'client.goods.icon': ['https://cdn.example/i.png'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps only matching orders for map_label and unknown types', () => {
|
||||||
|
const mixed = [
|
||||||
|
mapOrder,
|
||||||
|
{ ...mapOrder, order_id: '5', goods_type: 'license_year' },
|
||||||
|
] as any
|
||||||
|
|
||||||
|
expect(filterOrdersForProduct('map_label', mixed).map((item) => item.order_id)).toEqual(['4'])
|
||||||
|
expect(filterOrdersForProduct('', mixed).map((item) => item.order_id)).toEqual(['4'])
|
||||||
|
expect(filterOrdersForProduct('unknown', mixed)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('order address helpers', () => {
|
describe('order address helpers', () => {
|
||||||
it('splits named address and coordinate address', () => {
|
it('splits named address and coordinate address', () => {
|
||||||
expect(getOrderAddressName(mapOrder as any)).toBe('南京路1号')
|
expect(getOrderAddressName(mapOrder as any)).toBe('南京路1号')
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { isCorpGoodsType } from './detail'
|
||||||
|
|
||||||
export type UploadedImage = {
|
export type UploadedImage = {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -79,21 +81,30 @@ export function getGoodsIcons (order: OrderLike) {
|
||||||
return Array.isArray(icons) ? icons.filter((item) => typeof item === 'string') : []
|
return Array.isArray(icons) ? icons.filter((item) => typeof item === 'string') : []
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterMapLabelOrders<T extends OrderLike> (orders: T[] = []) {
|
export function normalizeOrders<T extends OrderLike> (orders: T[] = []) {
|
||||||
return orders
|
return orders.map((item) => {
|
||||||
.filter((item) => item.goods_type === 'map_label')
|
const nextItem = { ...item, extra: { ...(item.extra || {}) } }
|
||||||
.map((item) => {
|
const goodsParam = nextItem.extra.goods_param
|
||||||
const nextItem = { ...item, extra: { ...(item.extra || {}) } }
|
if (typeof goodsParam === 'string') {
|
||||||
const goodsParam = nextItem.extra.goods_param
|
try {
|
||||||
if (typeof goodsParam === 'string') {
|
nextItem.extra.goods_param = JSON.parse(goodsParam)
|
||||||
try {
|
} catch {
|
||||||
nextItem.extra.goods_param = JSON.parse(goodsParam)
|
nextItem.extra.goods_param = null
|
||||||
} catch {
|
|
||||||
nextItem.extra.goods_param = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nextItem
|
}
|
||||||
})
|
return nextItem
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterOrdersForProduct<T extends OrderLike> (goodsType: string, orders: T[] = []) {
|
||||||
|
const normalized = normalizeOrders(orders)
|
||||||
|
if (isCorpGoodsType(goodsType)) return normalized
|
||||||
|
const target = goodsType || 'map_label'
|
||||||
|
return normalized.filter((item) => item.goods_type === target)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterMapLabelOrders<T extends OrderLike> (orders: T[] = []) {
|
||||||
|
return filterOrdersForProduct('map_label', orders)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrderExtraImages (order: OrderLike | null | undefined, keys: string[]): UploadedImage[] {
|
export function getOrderExtraImages (order: OrderLike | null | undefined, keys: string[]): UploadedImage[] {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { mergeServiceConfig } from './contact'
|
import { mergeServiceConfig } from './contact'
|
||||||
|
import { pickGoodsType } from './detail'
|
||||||
import { resolveLaunchQuery, type LaunchQuery } from './launch'
|
import { resolveLaunchQuery, type LaunchQuery } from './launch'
|
||||||
import { hostFromOrigin, resolveApiOrigin } from './origin'
|
import { hostFromOrigin, resolveApiOrigin } from './origin'
|
||||||
import { createDeviceId } from './uuid'
|
import { createDeviceId } from './uuid'
|
||||||
|
|
@ -9,6 +10,7 @@ const SESSION_KEY = 'corpMpSession'
|
||||||
export type SessionState = LaunchQuery & {
|
export type SessionState = LaunchQuery & {
|
||||||
deviceId: string
|
deviceId: string
|
||||||
config: Record<string, any>
|
config: Record<string, any>
|
||||||
|
goodsType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_SESSION: SessionState = {
|
const EMPTY_SESSION: SessionState = {
|
||||||
|
|
@ -22,6 +24,7 @@ const EMPTY_SESSION: SessionState = {
|
||||||
phone: '',
|
phone: '',
|
||||||
deviceId: '',
|
deviceId: '',
|
||||||
config: {},
|
config: {},
|
||||||
|
goodsType: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
function readStorage (): Partial<SessionState> {
|
function readStorage (): Partial<SessionState> {
|
||||||
|
|
@ -96,10 +99,12 @@ export function applyCorpInit (payload: any) {
|
||||||
const data = payload?.data ?? payload
|
const data = payload?.data ?? payload
|
||||||
const token = typeof data?.token === 'string' ? data.token.trim() : ''
|
const token = typeof data?.token === 'string' ? data.token.trim() : ''
|
||||||
const packageId = data?.package === undefined || data?.package === null ? '' : String(data.package)
|
const packageId = data?.package === undefined || data?.package === null ? '' : String(data.package)
|
||||||
|
const goodsType = pickGoodsType(payload)
|
||||||
const patch: Partial<SessionState> = {
|
const patch: Partial<SessionState> = {
|
||||||
config: mergeServiceConfig(getSession().config, payload),
|
config: mergeServiceConfig(getSession().config, payload),
|
||||||
}
|
}
|
||||||
if (token) patch.token = token
|
if (token) patch.token = token
|
||||||
if (packageId) patch.packageId = packageId
|
if (packageId) patch.packageId = packageId
|
||||||
|
if (goodsType) patch.goodsType = goodsType
|
||||||
return setSession(patch)
|
return setSession(patch)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
import { Image, Text, View } from '@tarojs/components'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { RECEIPT_STAMP_DONE, RECEIPT_STAMP_PENDING } from '@/lib/cdn'
|
||||||
|
import {
|
||||||
|
getReceiptMeta,
|
||||||
|
numberToUpperCase,
|
||||||
|
shouldShowReceipt,
|
||||||
|
splitOrderFiles,
|
||||||
|
type OrderFile,
|
||||||
|
} from '@/lib/detail'
|
||||||
|
import type { OrderLike } from '@/lib/order'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
order: OrderLike
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DetailModal ({ order, onCancel }: Props) {
|
||||||
|
const showReceipt = shouldShowReceipt(order.goods_type)
|
||||||
|
const receipt = getReceiptMeta(order.goods_type, order.goods_name)
|
||||||
|
const files = order.process_status === '2' ? splitOrderFiles(order.files) : { images: [], pdfs: [] }
|
||||||
|
const empty = !showReceipt && files.images.length === 0 && files.pdfs.length === 0
|
||||||
|
const imageUrls = files.images.map((file) => String(file.url))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='detail-mask' onClick={onCancel}>
|
||||||
|
<View className='detail-dialog' onClick={(event) => event.stopPropagation()}>
|
||||||
|
<View className='detail-dialog__head'>
|
||||||
|
<Text className='detail-dialog__title'>详情</Text>
|
||||||
|
<View className='detail-dialog__close' onClick={onCancel}>
|
||||||
|
<Text className='detail-dialog__close-icon'>×</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='detail-dialog__body'>
|
||||||
|
{showReceipt ? <ReceiptCard order={order} title={receipt.title} description={receipt.description} /> : null}
|
||||||
|
|
||||||
|
{files.images.length ? (
|
||||||
|
<View className='detail-dialog__section'>
|
||||||
|
<Text className='detail-dialog__section-title'>完成截图:</Text>
|
||||||
|
{files.images.map((file, index) => (
|
||||||
|
<Image
|
||||||
|
key={file.id || file.url || index}
|
||||||
|
className='detail-dialog__shot'
|
||||||
|
src={String(file.url)}
|
||||||
|
mode='widthFix'
|
||||||
|
onClick={() => previewImages(imageUrls, String(file.url))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{files.pdfs.length ? (
|
||||||
|
<View className='detail-dialog__section'>
|
||||||
|
<Text className='detail-dialog__section-title'>报告文件:</Text>
|
||||||
|
{files.pdfs.map((file, index) => (
|
||||||
|
<View
|
||||||
|
key={file.id || file.url || index}
|
||||||
|
className='detail-dialog__pdf'
|
||||||
|
onClick={() => void openPdf(file)}
|
||||||
|
>
|
||||||
|
<Text className='detail-dialog__pdf-name'>查看报告文件{files.pdfs.length > 1 ? index + 1 : ''}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{empty ? (
|
||||||
|
<Text className='detail-dialog__empty'>暂无完成截图</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReceiptCard ({
|
||||||
|
order,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
order: OrderLike
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
}) {
|
||||||
|
const stamp = order.process_status === '2' ? RECEIPT_STAMP_DONE : RECEIPT_STAMP_PENDING
|
||||||
|
const remarkLines = description.split('\n').filter(Boolean)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='detail-dialog__section'>
|
||||||
|
<Text className='detail-dialog__section-title'>回执单:</Text>
|
||||||
|
<View className='detail-receipt'>
|
||||||
|
<Text className='detail-receipt__title'>{title}</Text>
|
||||||
|
<View className='detail-receipt__rule' />
|
||||||
|
<View className='detail-receipt__rule detail-receipt__rule--thin' />
|
||||||
|
|
||||||
|
<View className='detail-receipt__box'>
|
||||||
|
<View className='detail-receipt__inner'>
|
||||||
|
<View className='detail-receipt__fields'>
|
||||||
|
<Image className='detail-receipt__stamp' src={stamp} />
|
||||||
|
<ReceiptLine label='名称:' value={order.entity_name || ''} />
|
||||||
|
<ReceiptLine label='申报内容:' value={order.goods_name || ''} />
|
||||||
|
<ReceiptLine label='法人:' value={order.entity_legal || ''} />
|
||||||
|
<ReceiptLine label='已受理时间:' value={order.pay_time || ''} />
|
||||||
|
<ReceiptLine label='统一社会信用代码:' value={order.entity_id || ''} />
|
||||||
|
<ReceiptLine label='数量:' value='1' />
|
||||||
|
<ReceiptLine label='支付时间:' value={order.pay_time || ''} />
|
||||||
|
<ReceiptLine label='金额(小写):' value={order.total_fee ? `¥${order.total_fee}` : ''} />
|
||||||
|
<ReceiptLine label='订单号码:' value={order.out_trade_no || ''} />
|
||||||
|
<ReceiptLine label='金额(大写):' value={numberToUpperCase(order.total_fee)} />
|
||||||
|
</View>
|
||||||
|
<View className='detail-receipt__remark'>
|
||||||
|
<View className='detail-receipt__remark-label'>
|
||||||
|
<Text className='detail-receipt__remark-label-text'>备注</Text>
|
||||||
|
</View>
|
||||||
|
<View className='detail-receipt__remark-body'>
|
||||||
|
{remarkLines.map((line, index) => (
|
||||||
|
<Text key={`${index}-${line}`} className='detail-receipt__remark-line'>{line}</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReceiptLine ({ label, value }: { label: string, value: string }) {
|
||||||
|
return (
|
||||||
|
<View className='detail-receipt__row'>
|
||||||
|
<Text className='detail-receipt__label'>{label}</Text>
|
||||||
|
<Text className='detail-receipt__value'>{value}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewImages (urls: string[], current: string) {
|
||||||
|
if (!urls.length) return
|
||||||
|
Taro.previewImage({ current, urls })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPdf (file: OrderFile) {
|
||||||
|
const url = String(file.url || '')
|
||||||
|
if (!url) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
Taro.showLoading({ title: '打开中...', mask: true })
|
||||||
|
if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP) {
|
||||||
|
const downloaded = await Taro.downloadFile({ url })
|
||||||
|
if (downloaded.statusCode !== 200 || !downloaded.tempFilePath) {
|
||||||
|
throw new Error('download failed')
|
||||||
|
}
|
||||||
|
await Taro.openDocument({
|
||||||
|
filePath: downloaded.tempFilePath,
|
||||||
|
fileType: 'pdf',
|
||||||
|
showMenu: true,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Taro.setClipboardData({ data: url })
|
||||||
|
} catch {
|
||||||
|
Taro.showToast({ title: '报告打开失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
Taro.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,11 @@
|
||||||
.check-card {
|
.check-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin: -80px 24px 24px;
|
margin: 0 24px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-card--lift {
|
||||||
|
margin-top: -80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.check-card__header {
|
.check-card__header {
|
||||||
|
|
@ -272,39 +276,206 @@
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-mask {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-dialog {
|
.detail-dialog {
|
||||||
position: absolute;
|
box-sizing: border-box;
|
||||||
top: 50%;
|
width: 686px;
|
||||||
right: 32px;
|
max-height: 80vh;
|
||||||
left: 32px;
|
overflow: hidden;
|
||||||
padding: 32px 24px 24px;
|
border-radius: 20px;
|
||||||
border-radius: 16px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
transform: translateY(-50%);
|
}
|
||||||
|
|
||||||
|
.detail-dialog__head {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-dialog__title {
|
.detail-dialog__title {
|
||||||
display: block;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
color: #222;
|
color: #222;
|
||||||
font-size: 32px;
|
font-size: 34px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
text-align: center;
|
line-height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-dialog__close {
|
.detail-dialog__close {
|
||||||
margin-top: 32px;
|
position: absolute;
|
||||||
height: 80px;
|
top: 20px;
|
||||||
border: 0;
|
right: 20px;
|
||||||
border-radius: 8px;
|
display: flex;
|
||||||
background: #0261fc;
|
align-items: center;
|
||||||
color: #fff;
|
justify-content: center;
|
||||||
font-size: 30px;
|
width: 48px;
|
||||||
line-height: 80px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-dialog__close::after {
|
.detail-dialog__close-icon {
|
||||||
border: 0;
|
color: #999;
|
||||||
|
font-size: 48px;
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__body {
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: calc(80vh - 88px);
|
||||||
|
padding: 0 32px 40px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__section + .detail-dialog__section {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__section-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 34px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__shot {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background: #f3f3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__pdf {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 80px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #eaf3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__pdf-name {
|
||||||
|
color: #0261fc;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-dialog__empty {
|
||||||
|
display: block;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 28px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__title {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 52px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__rule {
|
||||||
|
width: 280px;
|
||||||
|
height: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__rule--thin {
|
||||||
|
height: 2px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__box {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 6px;
|
||||||
|
border: 2px solid #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__inner {
|
||||||
|
border: 2px solid #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__fields {
|
||||||
|
position: relative;
|
||||||
|
padding: 28px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__stamp {
|
||||||
|
position: absolute;
|
||||||
|
top: -16px;
|
||||||
|
right: 8px;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 36px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__remark {
|
||||||
|
display: flex;
|
||||||
|
border-top: 2px solid #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__remark-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
border-right: 2px solid #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__remark-label-text {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 22px;
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__remark-body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-receipt__remark-line {
|
||||||
|
display: block;
|
||||||
|
color: #666;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 32px;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.resubmit-mask {
|
.resubmit-mask {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ import Taro, { useDidShow, useLoad } from '@tarojs/taro'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { getOrdersByPhone } from '@/api/pay'
|
import { getOrdersByPhone } from '@/api/pay'
|
||||||
import { bootstrapSession } from '@/lib/bootstrap'
|
import { bootstrapSession } from '@/lib/bootstrap'
|
||||||
|
import { getSession } from '@/lib/session'
|
||||||
import { BANNER_BG, CARD_HEADER, CORP_ASSET_BASE, ENTITY_LABELS, HOME_ICON_BASE, PAGE_TITLE } from '@/lib/cdn'
|
import { BANNER_BG, CARD_HEADER, CORP_ASSET_BASE, ENTITY_LABELS, HOME_ICON_BASE, PAGE_TITLE } from '@/lib/cdn'
|
||||||
import { pickServiceContact, type ServiceContact } from '@/lib/contact'
|
import { pickServiceContact, type ServiceContact } from '@/lib/contact'
|
||||||
|
import { isCorpGoodsType } from '@/lib/detail'
|
||||||
import {
|
import {
|
||||||
filterMapLabelOrders,
|
filterOrdersForProduct,
|
||||||
getGoodsIcons,
|
getGoodsIcons,
|
||||||
getOrderAddressName,
|
getOrderAddressName,
|
||||||
parseCoordinate,
|
parseCoordinate,
|
||||||
|
|
@ -17,6 +19,7 @@ import { isValidPhone } from '@/lib/phone'
|
||||||
import { getStatusMeta, PAY_ICON } from '@/lib/status'
|
import { getStatusMeta, PAY_ICON } from '@/lib/status'
|
||||||
import { consumeMapPickerResult, type MapPickerResult } from '@/pages/map/picker'
|
import { consumeMapPickerResult, type MapPickerResult } from '@/pages/map/picker'
|
||||||
import ContactModal, { ContactEntry } from './contact'
|
import ContactModal, { ContactEntry } from './contact'
|
||||||
|
import DetailModal from './detail'
|
||||||
import ResubmitDrawer from './resubmit'
|
import ResubmitDrawer from './resubmit'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
|
|
@ -68,7 +71,7 @@ export default function Index () {
|
||||||
setChecking(true)
|
setChecking(true)
|
||||||
try {
|
try {
|
||||||
const res = await getOrdersByPhone(phone)
|
const res = await getOrdersByPhone(phone)
|
||||||
const nextOrders = filterMapLabelOrders(res.data || [])
|
const nextOrders = filterOrdersForProduct(getSession().goodsType, res.data || [])
|
||||||
setOrders(nextOrders)
|
setOrders(nextOrders)
|
||||||
setSearched(true)
|
setSearched(true)
|
||||||
if (!nextOrders.length) {
|
if (!nextOrders.length) {
|
||||||
|
|
@ -102,7 +105,7 @@ export default function Index () {
|
||||||
</View>
|
</View>
|
||||||
<ContactEntry onClick={() => setShowContact(true)} />
|
<ContactEntry onClick={() => setShowContact(true)} />
|
||||||
|
|
||||||
<View className='check-card'>
|
<View className='check-card check-card--lift'>
|
||||||
<View className='check-card__header'>
|
<View className='check-card__header'>
|
||||||
<Image className='check-card__header-bg' src={CARD_HEADER} mode='scaleToFill' />
|
<Image className='check-card__header-bg' src={CARD_HEADER} mode='scaleToFill' />
|
||||||
<Text className='check-card__header-title'>进度查询</Text>
|
<Text className='check-card__header-title'>进度查询</Text>
|
||||||
|
|
@ -169,17 +172,7 @@ export default function Index () {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{detailOrder ? (
|
{detailOrder ? (
|
||||||
<View className='detail-mask' onClick={() => setDetailOrder(null)}>
|
<DetailModal order={detailOrder} onCancel={() => setDetailOrder(null)} />
|
||||||
<View className='detail-dialog' onClick={(event) => event.stopPropagation()}>
|
|
||||||
<Text className='detail-dialog__title'>订单详情</Text>
|
|
||||||
<InfoLine label={`${ENTITY_LABELS.entityName}:`} value={detailOrder.entity_name || '-'} />
|
|
||||||
<InfoLine label='联系电话:' value={detailOrder.entity_phone || '-'} />
|
|
||||||
<InfoLine label={`${ENTITY_LABELS.entityAddress}:`} value={getOrderAddressName(detailOrder) || '-'} />
|
|
||||||
<InfoLine label={`${ENTITY_LABELS.entityMapLocation}:`} value={detailOrder.extra?.entity_address || '-'} />
|
|
||||||
<InfoLine label='办理说明:' value='各大平台门店管理审核时间一般为1-7个工作日,审核通过后可在对应地图平台查询。' />
|
|
||||||
<Button className='detail-dialog__close' onClick={() => setDetailOrder(null)}>关闭</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ContactModal
|
<ContactModal
|
||||||
|
|
@ -223,7 +216,9 @@ function OrderCard ({
|
||||||
const status = getStatusMeta(order.process_status, order.process_status_name)
|
const status = getStatusMeta(order.process_status, order.process_status_name)
|
||||||
const goodsIcons = getGoodsIcons(order)
|
const goodsIcons = getGoodsIcons(order)
|
||||||
const showDetail = order.process_status === '2'
|
const showDetail = order.process_status === '2'
|
||||||
|
const showResubmit = !isCorpGoodsType(order.goods_type)
|
||||||
const payIcon = PAY_ICON[order.pay_type]
|
const payIcon = PAY_ICON[order.pay_type]
|
||||||
|
const isCorpOrder = isCorpGoodsType(order.goods_type)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={`order-card ${first ? 'order-card--first' : ''}`}>
|
<View className={`order-card ${first ? 'order-card--first' : ''}`}>
|
||||||
|
|
@ -243,10 +238,21 @@ function OrderCard ({
|
||||||
<View className='order-card__reject'>驳回理由:{order.reject_reason}</View>
|
<View className='order-card__reject'>驳回理由:{order.reject_reason}</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<InfoLine label={`${ENTITY_LABELS.entityName}:`} value={order.entity_name || '-'} />
|
{isCorpOrder ? (
|
||||||
<InfoLine label='联系电话:' value={order.entity_phone || '-'} />
|
<>
|
||||||
<InfoLine label={`${ENTITY_LABELS.entityAddress}:`} value={getOrderAddressName(order) || '-'} />
|
<InfoLine label='企业名称:' value={order.entity_name || '-'} />
|
||||||
<InfoLine label={`${ENTITY_LABELS.entityMapLocation}:`} value={order.extra?.entity_address || '-'} />
|
<InfoLine label='法人:' value={order.entity_legal || '-'} />
|
||||||
|
<InfoLine label='统一社会信用代码:' value={order.entity_id || '-'} />
|
||||||
|
<InfoLine label='联系电话:' value={order.entity_phone || '-'} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityName}:`} value={order.entity_name || '-'} />
|
||||||
|
<InfoLine label='联系电话:' value={order.entity_phone || '-'} />
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityAddress}:`} value={getOrderAddressName(order) || '-'} />
|
||||||
|
<InfoLine label={`${ENTITY_LABELS.entityMapLocation}:`} value={order.extra?.entity_address || '-'} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<InfoLine label='订单时间:' value={order.create_time || order.pay_time || '-'} />
|
<InfoLine label='订单时间:' value={order.create_time || order.pay_time || '-'} />
|
||||||
|
|
||||||
<View className='order-card__row'>
|
<View className='order-card__row'>
|
||||||
|
|
@ -267,7 +273,9 @@ function OrderCard ({
|
||||||
{showDetail ? (
|
{showDetail ? (
|
||||||
<View className='order-card__action' onClick={onViewDetail}>查看详情</View>
|
<View className='order-card__action' onClick={onViewDetail}>查看详情</View>
|
||||||
) : null}
|
) : null}
|
||||||
<View className='order-card__action' onClick={onResubmit}>补充资料</View>
|
{showResubmit ? (
|
||||||
|
<View className='order-card__action' onClick={onResubmit}>补充资料</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue