From 0d795b622ccb5b1ee9b23bf8bf69ca78efdbc121 Mon Sep 17 00:00:00 2001 From: zhangjianjun Date: Fri, 21 Aug 2026 14:42:11 +0800 Subject: [PATCH] ud --- src/api/user.ts | 8 ++ src/lib/company.test.ts | 44 +++++++ src/lib/company.ts | 54 +++++++++ src/lib/order.test.ts | 6 +- src/lib/phone.test.ts | 10 +- src/lib/phone.ts | 5 + src/lib/status.test.ts | 15 ++- src/lib/status.ts | 5 +- src/pages/index/detail.tsx | 23 ++-- src/pages/index/index.tsx | 5 +- src/pages/resubmit/corp.tsx | 215 ++++++++++++++++++++++++++++++++++ src/pages/resubmit/index.scss | 135 +++++++++++++++++++++ src/pages/resubmit/index.tsx | 6 + 13 files changed, 516 insertions(+), 15 deletions(-) create mode 100644 src/lib/company.test.ts create mode 100644 src/lib/company.ts create mode 100644 src/pages/resubmit/corp.tsx diff --git a/src/api/user.ts b/src/api/user.ts index 6973fba..ab62926 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -22,6 +22,14 @@ export function initCorp (params: Record = {}) { }) } +export function getCompanies (keyword: string) { + return request({ + url: '/api/h5/company', + method: 'GET', + params: { keyword }, + }) +} + export function report (data: { contact: string content: string diff --git a/src/lib/company.test.ts b/src/lib/company.test.ts new file mode 100644 index 0000000..b3daad4 --- /dev/null +++ b/src/lib/company.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { buildCorpResubmitPayload, getCorpResubmitValues, validateIDCard } from './company' + +describe('corp resubmit', () => { + const order = { + order_id: '12', + entity_name: '测试有限公司', + entity_id: '91330100MA2TEST001', + entity_legal: '张三', + entity_idno: '11010519491231002X', + entity_phone: '13800138000', + } + + it('initializes the company form from the queried order', () => { + expect(getCorpResubmitValues(order)).toEqual({ + entity_name: '测试有限公司', + entity_id: '91330100MA2TEST001', + entity_legal: '张三', + entity_idno: '11010519491231002X', + entity_phone: '13800138000', + }) + }) + + it('matches the H5 update-order payload', () => { + expect(buildCorpResubmitPayload(order, getCorpResubmitValues(order))).toEqual({ + id: '12', + entity_extra: {}, + entity_id: '91330100MA2TEST001', + entity_idno: '11010519491231002X', + entity_legal: '张三', + entity_name: '测试有限公司', + entity_phone: '13800138000', + }) + }) +}) + +describe('validateIDCard', () => { + it('validates the 18 digit checksum and accepts a lowercase x', () => { + expect(validateIDCard('11010519491231002X')).toBe(true) + expect(validateIDCard('11010519491231002x')).toBe(true) + expect(validateIDCard('110105194912310021')).toBe(false) + expect(validateIDCard('not-an-id')).toBe(false) + }) +}) diff --git a/src/lib/company.ts b/src/lib/company.ts new file mode 100644 index 0000000..dc284a3 --- /dev/null +++ b/src/lib/company.ts @@ -0,0 +1,54 @@ +import type { OrderLike } from './order' + +export type CompanyItem = { + name: string + merge_name?: string + color_name?: string + credit_code: string + oper_name: string + start_date?: string + status?: string +} + +export type CorpResubmitValues = { + entity_name: string + entity_id: string + entity_legal: string + entity_idno: string + entity_phone: string +} + +export function getCorpResubmitValues (order: OrderLike): CorpResubmitValues { + return { + entity_name: String(order.entity_name || ''), + entity_id: String(order.entity_id || ''), + entity_legal: String(order.entity_legal || ''), + entity_idno: String(order.entity_idno || ''), + entity_phone: String(order.entity_phone || ''), + } +} + +export function validateIDCard (id: string) { + const normalized = String(id || '').trim().toUpperCase() + if (!/^\d{17}[\dX]$/.test(normalized)) return false + + const coefficients = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + const sum = coefficients.reduce((total, coefficient, index) => { + return total + Number(normalized[index]) * coefficient + }, 0) + + return checkCodes[sum % 11] === normalized[17] +} + +export function buildCorpResubmitPayload (order: OrderLike, values: CorpResubmitValues) { + return { + id: String(order.order_id || ''), + entity_extra: {}, + entity_id: values.entity_id, + entity_idno: values.entity_idno, + entity_legal: values.entity_legal, + entity_name: values.entity_name, + entity_phone: values.entity_phone, + } +} diff --git a/src/lib/order.test.ts b/src/lib/order.test.ts index 456f4e1..8a8e911 100644 --- a/src/lib/order.test.ts +++ b/src/lib/order.test.ts @@ -45,13 +45,15 @@ describe('filterMapLabelOrders', () => { }) describe('filterOrdersForProduct', () => { - it('keeps every order when querying from a corp goods type', () => { + it('keeps all three corp products together when querying from a corp entry', () => { const orders = filterOrdersForProduct('license_year', [ mapOrder, { ...mapOrder, order_id: '5', goods_type: 'license_year' }, + { ...mapOrder, order_id: '6', goods_type: 'license_destory' }, + { ...mapOrder, order_id: '7', goods_type: 'credit_repair' }, ] as any) - expect(orders.map((item) => item.order_id)).toEqual(['4', '5']) + expect(orders.map((item) => item.order_id)).toEqual(['4', '5', '6', '7']) expect(orders[0].extra.goods_param).toEqual({ 'client.goods.icon': ['https://cdn.example/i.png'], }) diff --git a/src/lib/phone.test.ts b/src/lib/phone.test.ts index 715959e..e7148d8 100644 --- a/src/lib/phone.test.ts +++ b/src/lib/phone.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { isValidPhone } from './phone' +import { isValidMobilePhone, isValidPhone } from './phone' describe('isValidPhone', () => { it('accepts mainland mobile numbers', () => { @@ -16,3 +16,11 @@ describe('isValidPhone', () => { expect(isValidPhone('23800138000')).toBe(false) }) }) + +describe('isValidMobilePhone', () => { + it('keeps corp forms limited to mainland mobile numbers', () => { + expect(isValidMobilePhone('13800138000')).toBe(true) + expect(isValidMobilePhone('010-12345678')).toBe(false) + expect(isValidMobilePhone('01012345678')).toBe(false) + }) +}) diff --git a/src/lib/phone.ts b/src/lib/phone.ts index d9571c1..ca269e0 100644 --- a/src/lib/phone.ts +++ b/src/lib/phone.ts @@ -1,5 +1,10 @@ +const MOBILE_PHONE_PATTERN = /^1[3-9]\d{9}$/ const PHONE_PATTERN = /^(1[3-9]\d{9}|0\d{2,3}-?\d{7,8})$/ +export function isValidMobilePhone (phone: string) { + return MOBILE_PHONE_PATTERN.test(String(phone || '').trim()) +} + export function isValidPhone (phone: string) { return PHONE_PATTERN.test(String(phone || '').trim()) } diff --git a/src/lib/status.test.ts b/src/lib/status.test.ts index 714420f..622f134 100644 --- a/src/lib/status.test.ts +++ b/src/lib/status.test.ts @@ -74,7 +74,18 @@ describe('getOrderCardActions', () => { }) }) - it('hides submit for corp goods even when status would otherwise allow it', () => { - expect(getOrderCardActions({ process_status: '4', goods_type: 'license_year' }).submit).toBe(false) + it('matches H5 actions for all three corp goods types', () => { + for (const goodsType of ['license_year', 'license_destory', 'credit_repair']) { + expect(getOrderCardActions({ process_status: '6', goods_type: goodsType })).toEqual({ + complaint: true, + submit: true, + detail: false, + }) + expect(getOrderCardActions({ process_status: '4', goods_type: goodsType })).toEqual({ + complaint: false, + submit: false, + detail: true, + }) + } }) }) diff --git a/src/lib/status.ts b/src/lib/status.ts index d2a37fe..9ac5027 100644 --- a/src/lib/status.ts +++ b/src/lib/status.ts @@ -48,10 +48,11 @@ export function filterOrdersByTab> (orders: T[] = export function getOrderCardActions (order: { process_status?: string, goods_type?: string }) { const status = String(order.process_status || '') - const detail = DONE_STATUSES.has(status) + const corpGoods = isCorpGoodsType(order.goods_type) + const detail = corpGoods ? status !== '6' : DONE_STATUSES.has(status) return { complaint: !detail, - submit: SUBMIT_STATUSES.has(status) && !isCorpGoodsType(order.goods_type), + submit: corpGoods ? status === '6' : SUBMIT_STATUSES.has(status), detail, } } diff --git a/src/pages/index/detail.tsx b/src/pages/index/detail.tsx index 39cd41d..62fc41e 100644 --- a/src/pages/index/detail.tsx +++ b/src/pages/index/detail.tsx @@ -26,7 +26,7 @@ export default function DetailDrawer ({ 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 files = splitOrderFiles(order.files) const empty = !showReceipt && files.images.length === 0 && files.pdfs.length === 0 const imageUrls = files.images.map((file) => String(file.url)) @@ -125,15 +125,18 @@ function ReceiptCard ({ - - - + + + - - - + + + @@ -152,6 +155,12 @@ function ReceiptCard ({ ) } +function formatAmount (amount: unknown) { + if (amount === '' || amount === undefined || amount === null) return '' + const value = Number(amount) + return Number.isFinite(value) ? `¥${value.toFixed(2)}` : `¥${String(amount)}` +} + function ReceiptLine ({ label, value }: { label: string, value: string }) { return ( diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 171e1a4..b12c24d 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -5,6 +5,7 @@ import { getOrdersByPhone } from '@/api/pay' import { LOCAL_IMAGES } from '@/lib/assets' import { bootstrapSession } from '@/lib/bootstrap' import { COPY_ICON, PAGE_TITLE } from '@/lib/cdn' +import { isCorpGoodsType } from '@/lib/detail' import { getNavMetrics } from '@/lib/nav' import { pickServiceContact, type ServiceContact } from '@/lib/contact' import { setOrderDraft } from '@/lib/orderDraft' @@ -238,7 +239,9 @@ function OrderCard ({ 查看详情 ) : null} {actions.submit ? ( - 补充资料 + + {isCorpGoodsType(order.goods_type) ? '重新提交' : '补充资料'} + ) : null} diff --git a/src/pages/resubmit/corp.tsx b/src/pages/resubmit/corp.tsx new file mode 100644 index 0000000..3c6e293 --- /dev/null +++ b/src/pages/resubmit/corp.tsx @@ -0,0 +1,215 @@ +import { Button, Input, Text, View } from '@tarojs/components' +import Taro from '@tarojs/taro' +import { useState } from 'react' +import { updateOrderExtra } from '@/api/pay' +import { getCompanies } from '@/api/user' +import { + buildCorpResubmitPayload, + getCorpResubmitValues, + validateIDCard, + type CompanyItem, + type CorpResubmitValues, +} from '@/lib/company' +import { isValidMobilePhone } from '@/lib/phone' +import type { OrderLike } from '@/lib/order' + +type Props = { + order: OrderLike +} + +export default function CorpResubmit ({ order }: Props) { + const [values, setValues] = useState(() => getCorpResubmitValues(order)) + const [companies, setCompanies] = useState([]) + const [querying, setQuerying] = useState(false) + const [submitting, setSubmitting] = useState(false) + + const updateField = (field: K, value: CorpResubmitValues[K]) => { + setValues((current) => ({ ...current, [field]: value })) + } + + const handleCompanyNameChange = (value: string) => { + setValues((current) => ({ + ...current, + entity_name: value, + entity_id: '', + entity_legal: '', + })) + setCompanies([]) + } + + const handleQueryCompany = async () => { + const keyword = values.entity_name.replace(/\s/g, '') + if (!keyword) { + Taro.showToast({ title: '查询关键字不能为空', icon: 'none' }) + return + } + if (querying) return + + setQuerying(true) + try { + const res = await getCompanies(keyword) + const list = (Array.isArray(res.data) ? res.data : []) + .filter((item: CompanyItem) => item?.credit_code) + setCompanies(list) + if (!list.length) { + Taro.showToast({ title: '未查询到公司', icon: 'none' }) + } + } catch { + setCompanies([]) + Taro.showToast({ title: '查询公司失败,请稍后再试', icon: 'none' }) + } finally { + setQuerying(false) + } + } + + const handleSelectCompany = (company: CompanyItem) => { + setValues((current) => ({ + ...current, + entity_name: company.name, + entity_id: company.credit_code, + entity_legal: company.oper_name, + })) + setCompanies([]) + } + + const handleSubmit = async () => { + if (submitting) return + if (!values.entity_name || !values.entity_id || !values.entity_legal) { + Taro.showToast({ title: '请查询并选择公司', icon: 'none' }) + return + } + if (!values.entity_idno || !values.entity_phone) { + Taro.showToast({ title: '请填写完整信息', icon: 'none' }) + return + } + if (!validateIDCard(values.entity_idno)) { + Taro.showToast({ title: '请填写正确身份证号', icon: 'none' }) + return + } + if (!isValidMobilePhone(values.entity_phone)) { + Taro.showToast({ title: '请填写正确手机号', icon: 'none' }) + return + } + + setSubmitting(true) + try { + await updateOrderExtra(buildCorpResubmitPayload(order, values)) + Taro.showToast({ title: '重新提交成功', icon: 'success' }) + setTimeout(() => Taro.navigateBack(), 500) + } catch { + Taro.showToast({ title: '重新提交失败,请稍后再试', icon: 'none' }) + } finally { + setSubmitting(false) + } + } + + return ( + + {order.reject_reason ? ( + 驳回理由:{order.reject_reason} + ) : null} + + + + + + + handleCompanyNameChange(event.detail.value)} + /> + + + + {companies.length ? ( + + {companies.map((company, index) => ( + handleSelectCompany(company)} + > + {company.name} + {company.oper_name} + + ))} + + ) : null} + + + {values.entity_id ? ( + + + {values.entity_name} + ({values.entity_id}) + + {values.entity_legal} + + ) : null} + + + + + { + const value = event.detail.value.toUpperCase().replace(/[^\dX]/g, '').slice(0, 18) + updateField('entity_idno', value) + }} + /> + + + + + + + updateField('entity_phone', event.detail.value.replace(/\D/g, '').slice(0, 11))} + /> + + + + + + + + + + ) +} + +function CorpFieldLabel ({ text }: { text: string }) { + return ( + + * + {text} + + ) +} diff --git a/src/pages/resubmit/index.scss b/src/pages/resubmit/index.scss index 2cc7ade..0510608 100644 --- a/src/pages/resubmit/index.scss +++ b/src/pages/resubmit/index.scss @@ -247,3 +247,138 @@ .resubmit__submit::after { border: 0; } + +.resubmit-page--corp .resubmit-card { + padding-bottom: 12px; +} + +.resubmit-page--corp .resubmit__field { + margin-bottom: 36px; +} + +.resubmit__required--leading { + margin-right: 6px; + margin-left: 0; +} + +.resubmit__control--company { + padding-right: 12px; +} + +.resubmit__query-company { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + width: 120px; + height: 64px; + margin: 0 0 0 12px; + padding: 0; + border: 0; + border-radius: 12px; + background: #fc6c00; + color: #fff; + font-size: 26px; + line-height: 64px; +} + +.resubmit__query-company::after { + border: 0; +} + +.resubmit__company-list { + position: relative; + z-index: 2; + max-height: 420px; + margin-top: 8px; + overflow-y: auto; + border-radius: 12px; + background: #fff; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12); +} + +.resubmit__company-item { + display: flex; + align-items: center; + box-sizing: border-box; + min-height: 96px; + padding: 16px 20px; + border-bottom: 2px solid #eee; +} + +.resubmit__company-item:last-child { + border-bottom: 0; +} + +.resubmit__company-name { + flex: 1; + min-width: 0; + color: #1a1a1a; + font-size: 26px; + line-height: 36px; +} + +.resubmit__company-legal { + flex-shrink: 0; + max-width: 160px; + margin-left: 20px; + overflow: hidden; + color: #fc6c00; + font-size: 24px; + line-height: 36px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.resubmit__selected-company { + display: flex; + align-items: center; + justify-content: space-between; + box-sizing: border-box; + min-height: 128px; + margin: -16px 0 36px; + padding: 20px 24px; + border-radius: 12px; + background: rgba(252, 108, 0, 0.06); +} + +.resubmit__selected-company-main { + display: flex; + flex: 1; + min-width: 0; + flex-direction: column; +} + +.resubmit__selected-company-name, +.resubmit__selected-company-code, +.resubmit__selected-company-legal { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.resubmit__selected-company-name { + color: #af5715; + font-size: 28px; + font-weight: 600; + line-height: 40px; +} + +.resubmit__selected-company-code { + margin-top: 4px; + color: #fc6c00; + font-size: 24px; + line-height: 34px; +} + +.resubmit__selected-company-legal { + flex-shrink: 0; + max-width: 180px; + margin-left: 20px; + color: #8b7c6f; + font-size: 26px; +} + +.resubmit__submit--corp { + background: #df0000; +} diff --git a/src/pages/resubmit/index.tsx b/src/pages/resubmit/index.tsx index 95b6141..6341ca8 100644 --- a/src/pages/resubmit/index.tsx +++ b/src/pages/resubmit/index.tsx @@ -5,6 +5,7 @@ import { updateOrderExtra } from '@/api/pay' import { uploadImage } from '@/api/user' import { LOCAL_IMAGES } from '@/lib/assets' import { ENTITY_LABELS, HOME_ICON_BASE, INPUT_ARROW_ICON, MAP_ASSET_BASE } from '@/lib/cdn' +import { isCorpGoodsType } from '@/lib/detail' import { getOrderDraft } from '@/lib/orderDraft' import { isValidPhone } from '@/lib/phone' import { @@ -23,6 +24,7 @@ import { type UploadedImage, } from '@/lib/order' import { consumeMapPickerResult } from '@/pages/map/picker' +import CorpResubmit from './corp' import './index.scss' const STOREFRONT_LABELS = ['门头正面', '门头左面', '门头右面'] @@ -257,6 +259,10 @@ export default function ResubmitPage () { const mapPoint = parseCoordinate(values.entity_address) const mapDisplay = mapPoint ? formatMapCoordinateDisplay(mapPoint) : (values.entity_address || '') + if (order && isCorpGoodsType(order.goods_type)) { + return + } + return ( {order?.reject_reason ? (