This commit is contained in:
zhangjianjun 2026-08-21 14:42:11 +08:00
parent b30dcfbafb
commit 0d795b622c
13 changed files with 516 additions and 15 deletions

View File

@ -22,6 +22,14 @@ export function initCorp (params: Record<string, any> = {}) {
})
}
export function getCompanies (keyword: string) {
return request({
url: '/api/h5/company',
method: 'GET',
params: { keyword },
})
}
export function report (data: {
contact: string
content: string

44
src/lib/company.test.ts Normal file
View File

@ -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)
})
})

54
src/lib/company.ts Normal file
View File

@ -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,
}
}

View File

@ -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'],
})

View File

@ -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)
})
})

View File

@ -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())
}

View File

@ -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,
})
}
})
})

View File

@ -48,10 +48,11 @@ export function filterOrdersByTab<T extends Record<string, any>> (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,
}
}

View File

@ -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 ({
<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_legal || ''}${order.entity_phone ? ` ${order.entity_phone}` : ''}`}
/>
<ReceiptLine label='统一社会信用代码:' value={order.entity_id || ''} />
<ReceiptLine label='已受理时间:' value={order.pay_time || ''} />
<ReceiptLine label='申报内容:' value={order.goods_name || ''} />
<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={formatAmount(order.total_fee)} />
<ReceiptLine label='金额(大写):' value={numberToUpperCase(order.total_fee)} />
<ReceiptLine label='支付时间:' value={order.pay_time || ''} />
<ReceiptLine label='订单号码:' value={order.out_trade_no || ''} />
</View>
<View className='detail-receipt__remark'>
<View className='detail-receipt__remark-label'>
@ -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 (
<View className='detail-receipt__row'>

View File

@ -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 ({
<View className='order-card__action' onClick={onViewDetail}></View>
) : null}
{actions.submit ? (
<View className='order-card__action order-card__action--primary' onClick={onResubmit}></View>
<View className='order-card__action order-card__action--primary' onClick={onResubmit}>
{isCorpGoodsType(order.goods_type) ? '重新提交' : '补充资料'}
</View>
) : null}
</View>
</>

215
src/pages/resubmit/corp.tsx Normal file
View File

@ -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<CorpResubmitValues>(() => getCorpResubmitValues(order))
const [companies, setCompanies] = useState<CompanyItem[]>([])
const [querying, setQuerying] = useState(false)
const [submitting, setSubmitting] = useState(false)
const updateField = <K extends keyof CorpResubmitValues>(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 (
<View className='resubmit-page resubmit-page--corp'>
{order.reject_reason ? (
<View className='resubmit-page__reject'>{order.reject_reason}</View>
) : null}
<View className='resubmit-page__body'>
<View className='resubmit-card'>
<View className='resubmit__field resubmit__field--company'>
<CorpFieldLabel text='公司名称' />
<View className='resubmit__control resubmit__control--company'>
<Input
className='resubmit__input'
value={values.entity_name}
placeholder='请输入营业执照名称'
placeholderClass='resubmit__placeholder'
onInput={(event) => handleCompanyNameChange(event.detail.value)}
/>
<Button
className='resubmit__query-company'
loading={querying}
disabled={querying}
onClick={() => void handleQueryCompany()}
>
{querying ? '查询中' : '查询'}
</Button>
</View>
{companies.length ? (
<View className='resubmit__company-list'>
{companies.map((company, index) => (
<View
className='resubmit__company-item'
key={`${company.credit_code}-${index}`}
onClick={() => handleSelectCompany(company)}
>
<Text className='resubmit__company-name'>{company.name}</Text>
<Text className='resubmit__company-legal'>{company.oper_name}</Text>
</View>
))}
</View>
) : null}
</View>
{values.entity_id ? (
<View className='resubmit__selected-company'>
<View className='resubmit__selected-company-main'>
<Text className='resubmit__selected-company-name'>{values.entity_name}</Text>
<Text className='resubmit__selected-company-code'>({values.entity_id})</Text>
</View>
<Text className='resubmit__selected-company-legal'>{values.entity_legal}</Text>
</View>
) : null}
<View className='resubmit__field'>
<CorpFieldLabel text='身份证号' />
<View className='resubmit__control'>
<Input
className='resubmit__input'
maxlength={18}
value={values.entity_idno}
placeholder='请输入法人身份证号'
placeholderClass='resubmit__placeholder'
onInput={(event) => {
const value = event.detail.value.toUpperCase().replace(/[^\dX]/g, '').slice(0, 18)
updateField('entity_idno', value)
}}
/>
</View>
</View>
<View className='resubmit__field'>
<CorpFieldLabel text='手机号码' />
<View className='resubmit__control'>
<Input
className='resubmit__input'
type='number'
maxlength={11}
value={values.entity_phone}
placeholder='请输入法人手机号'
placeholderClass='resubmit__placeholder'
onInput={(event) => updateField('entity_phone', event.detail.value.replace(/\D/g, '').slice(0, 11))}
/>
</View>
</View>
</View>
</View>
<View className='resubmit-page__footer'>
<Button
className='resubmit__submit resubmit__submit--corp'
loading={submitting}
disabled={submitting}
onClick={() => void handleSubmit()}
>
</Button>
</View>
</View>
)
}
function CorpFieldLabel ({ text }: { text: string }) {
return (
<View className='resubmit__label'>
<Text className='resubmit__required resubmit__required--leading'>*</Text>
<Text>{text}</Text>
</View>
)
}

View File

@ -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;
}

View File

@ -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 <CorpResubmit order={order} />
}
return (
<View className='resubmit-page'>
{order?.reject_reason ? (