665 lines
22 KiB
TypeScript
665 lines
22 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
||
import {
|
||
Alert,
|
||
App,
|
||
Button,
|
||
Card,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
QRCode,
|
||
Radio,
|
||
Select,
|
||
Space,
|
||
Tag,
|
||
Typography,
|
||
} from 'antd'
|
||
|
||
import { EvidenceUpload } from '@/components/EvidenceUpload'
|
||
import {
|
||
acceptCheckout,
|
||
cancelMyOrder,
|
||
cancelOrderDispute,
|
||
confirmCheckout,
|
||
confirmReceive,
|
||
counterCheckout as counterCheckoutApi,
|
||
createOrderDispute,
|
||
queryOrderPayment,
|
||
startOrderPayment,
|
||
submitCheckout as submitCheckoutApi,
|
||
submitHandoff as submitHandoffApi,
|
||
} from '@/features/orders/api'
|
||
import {
|
||
DISPUTE_TYPES,
|
||
getOrderActionAvailability,
|
||
splitEvidenceLines,
|
||
toPositiveNumber,
|
||
yuanToCent,
|
||
} from '@/features/orders/orderActions'
|
||
import type { UserOrderDetail } from '@/features/orders/types'
|
||
import { useSessionStore } from '@/stores/session'
|
||
import { confirmModal } from '@/shared/utils/confirm'
|
||
import { formatMoney } from '@/shared/utils/format'
|
||
|
||
type DialogKind = 'pay' | 'handoff' | 'checkout' | 'counter' | 'dispute' | null
|
||
|
||
interface ActionFormValues {
|
||
payWay?: 'WXZF' | 'ZFBZF'
|
||
content?: string
|
||
consumableYuan?: number
|
||
coinConsumed?: number
|
||
otherYuan?: number
|
||
reason?: string
|
||
evidenceText?: string
|
||
evidenceUrls?: string[]
|
||
disputeType?: string
|
||
}
|
||
|
||
interface OrderActionPanelProps {
|
||
order: UserOrderDetail
|
||
onChanged: () => void | Promise<void>
|
||
autoPay?: boolean
|
||
onAutoPayHandled?: () => void
|
||
}
|
||
|
||
export function OrderActionPanel({
|
||
order,
|
||
onChanged,
|
||
autoPay = false,
|
||
onAutoPayHandled,
|
||
}: OrderActionPanelProps) {
|
||
const { message, modal } = App.useApp()
|
||
const [form] = Form.useForm<ActionFormValues>()
|
||
const user = useSessionStore(state => state.user)
|
||
const [dialog, setDialog] = useState<DialogKind>(null)
|
||
const [busy, setBusy] = useState<string | null>(null)
|
||
const [payURL, setPayURL] = useState('')
|
||
const autoPayTriggered = useRef(false)
|
||
const payPolling = useRef<number | null>(null)
|
||
const actions = getOrderActionAvailability(order, user?.id)
|
||
const checkout = order.checkout
|
||
|
||
const closeDialog = () => {
|
||
if (busy) return
|
||
setDialog(null)
|
||
setPayURL('')
|
||
form.resetFields()
|
||
}
|
||
|
||
const readForm = async (): Promise<ActionFormValues | null> => {
|
||
try {
|
||
return await form.validateFields()
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
const evidencePayload = (values: ActionFormValues) => [
|
||
...(values.evidenceUrls ?? []),
|
||
...splitEvidenceLines(values.evidenceText ?? ''),
|
||
]
|
||
|
||
const openDialog = (kind: Exclude<DialogKind, null>) => {
|
||
form.resetFields()
|
||
setPayURL('')
|
||
if (kind === 'pay') {
|
||
form.setFieldsValue({ payWay: 'WXZF' })
|
||
}
|
||
if (kind === 'checkout') {
|
||
form.setFieldsValue({
|
||
content: '',
|
||
consumableYuan: 0,
|
||
coinConsumed: checkout?.coin_consumed_m ?? 0,
|
||
otherYuan: 0,
|
||
evidenceText: '',
|
||
evidenceUrls: [],
|
||
})
|
||
}
|
||
if (kind === 'counter') {
|
||
form.setFieldsValue({
|
||
consumableYuan: (checkout?.consumable_amount_cent ?? 0) / 100,
|
||
coinConsumed: checkout?.coin_consumed_m ?? 0,
|
||
otherYuan: (checkout?.deposit_deduct_amount_cent ?? 0) / 100,
|
||
reason: '',
|
||
evidenceText: '',
|
||
evidenceUrls: [],
|
||
})
|
||
}
|
||
if (kind === 'dispute') {
|
||
form.setFieldsValue({
|
||
disputeType: actions.checkoutDisputeStage ? 'checkout_dispute' : 'cannot_login',
|
||
content: '',
|
||
evidenceText: '',
|
||
evidenceUrls: [],
|
||
})
|
||
}
|
||
setDialog(kind)
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (!autoPay || autoPayTriggered.current || !actions.canPay) return
|
||
autoPayTriggered.current = true
|
||
openDialog('pay')
|
||
onAutoPayHandled?.()
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [autoPay, actions.canPay])
|
||
|
||
useEffect(() => {
|
||
if (dialog !== 'pay' || !payURL) return
|
||
|
||
let cancelled = false
|
||
let attempts = 0
|
||
const poll = async () => {
|
||
if (cancelled) return
|
||
attempts += 1
|
||
try {
|
||
const payment = await queryOrderPayment(order.id)
|
||
if (cancelled) return
|
||
if (payment.paid) {
|
||
message.success('支付成功,订单已进入待交接')
|
||
setDialog(null)
|
||
setPayURL('')
|
||
await onChanged()
|
||
return
|
||
}
|
||
} catch {
|
||
// 自动轮询不打断用户;保留手动检查作为明确反馈。
|
||
}
|
||
if (!cancelled && attempts < 200) {
|
||
payPolling.current = window.setTimeout(() => void poll(), 3000)
|
||
}
|
||
}
|
||
|
||
payPolling.current = window.setTimeout(() => void poll(), 3000)
|
||
return () => {
|
||
cancelled = true
|
||
if (payPolling.current !== null) {
|
||
window.clearTimeout(payPolling.current)
|
||
payPolling.current = null
|
||
}
|
||
}
|
||
}, [dialog, message, onChanged, order.id, payURL])
|
||
|
||
const run = async (key: string, fn: () => Promise<void>) => {
|
||
setBusy(key)
|
||
try {
|
||
await fn()
|
||
await onChanged()
|
||
setDialog(null)
|
||
setPayURL('')
|
||
} catch (error) {
|
||
message.error(error instanceof Error ? error.message : '操作失败,请稍后再试')
|
||
} finally {
|
||
setBusy(null)
|
||
}
|
||
}
|
||
|
||
const submitPay = async () => {
|
||
const values = await readForm()
|
||
if (!values) return
|
||
setBusy('pay')
|
||
try {
|
||
const payment = await startOrderPayment(order.id, values.payWay ?? 'WXZF')
|
||
if (payment.paid) {
|
||
message.success('支付成功')
|
||
await onChanged()
|
||
setDialog(null)
|
||
setPayURL('')
|
||
return
|
||
}
|
||
const url = payment.jspay_url || payment.td_code || payment.jspay_info || ''
|
||
if (url) {
|
||
setPayURL(url)
|
||
message.info('支付单已创建,系统会自动检查支付结果')
|
||
} else {
|
||
message.warning('支付单已创建,但渠道没有返回支付链接,请稍后检查支付状态')
|
||
}
|
||
} catch (error) {
|
||
message.error(error instanceof Error ? error.message : '发起支付失败')
|
||
} finally {
|
||
setBusy(null)
|
||
}
|
||
}
|
||
|
||
const checkPayment = async () => {
|
||
setBusy('check-pay')
|
||
try {
|
||
const payment = await queryOrderPayment(order.id)
|
||
if (payment.paid) {
|
||
message.success('支付成功,订单已进入待交接')
|
||
setDialog(null)
|
||
setPayURL('')
|
||
await onChanged()
|
||
return
|
||
}
|
||
message.info('支付暂未完成,请稍后再试')
|
||
} catch (error) {
|
||
message.error(error instanceof Error ? error.message : '查询支付状态失败')
|
||
} finally {
|
||
setBusy(null)
|
||
}
|
||
}
|
||
|
||
const submitHandoff = async () => {
|
||
const values = await readForm()
|
||
if (!values) return
|
||
await run('handoff', async () => {
|
||
await submitHandoffApi(order.id, values.content ?? '')
|
||
message.success('交接说明已提交')
|
||
})
|
||
}
|
||
|
||
const submitReceive = async () => {
|
||
const confirmed = await confirmModal(modal, {
|
||
title: '确认收号',
|
||
content: '请确认账号可以正常登录、资料与交接说明一致。确认后订单进入使用中。',
|
||
okText: '确认收号',
|
||
cancelText: '再检查一下',
|
||
})
|
||
if (!confirmed) return
|
||
await run('receive', async () => {
|
||
await confirmReceive(order.id)
|
||
message.success('已确认收号')
|
||
})
|
||
}
|
||
|
||
const submitCheckout = async () => {
|
||
const values = await readForm()
|
||
if (!values) return
|
||
const confirmed = await confirmModal(modal, {
|
||
title: '确认提交结账',
|
||
content: '结账会直接影响租金、押金赔付与双方结算金额,提交后将发送给对方确认。',
|
||
okText: '提交结账',
|
||
cancelText: '再检查一下',
|
||
})
|
||
if (!confirmed) return
|
||
await run('checkout', async () => {
|
||
await submitCheckoutApi(order.id, {
|
||
content: values.content ?? '',
|
||
consumable_amount_cent: yuanToCent(values.consumableYuan ?? 0),
|
||
coin_consumed_m: toPositiveNumber(values.coinConsumed ?? 0),
|
||
other_amount_cent: yuanToCent(values.otherYuan ?? 0),
|
||
evidence_urls: evidencePayload(values),
|
||
})
|
||
message.success('结账已发起,等待对方确认')
|
||
})
|
||
}
|
||
|
||
const submitConfirmCheckout = async () => {
|
||
if ((checkout?.shortfall_cent ?? 0) > 0) {
|
||
message.warning('押金不足,请修改方案或发起争议')
|
||
return
|
||
}
|
||
const confirmed = await confirmModal(modal, {
|
||
title: '确认结账',
|
||
content: '确认账号状态和扣款金额无误吗?确认后订单将完成。',
|
||
okText: '确认结账',
|
||
cancelText: '再检查一下',
|
||
})
|
||
if (!confirmed) return
|
||
await run('confirm-checkout', async () => {
|
||
await confirmCheckout(order.id)
|
||
message.success('结账已确认,订单完成')
|
||
})
|
||
}
|
||
|
||
const submitAcceptCheckout = async () => {
|
||
if ((checkout?.shortfall_cent ?? 0) > 0) {
|
||
message.warning('押金不足,请修改方案或发起争议')
|
||
return
|
||
}
|
||
const confirmed = await confirmModal(modal, {
|
||
title: '同意结账方案',
|
||
content: '确定同意当前结账方案吗?同意后订单将完成结算。',
|
||
okText: '同意结账',
|
||
cancelText: '再检查一下',
|
||
})
|
||
if (!confirmed) return
|
||
await run('accept-checkout', async () => {
|
||
await acceptCheckout(order.id)
|
||
message.success('已同意结账,订单完成')
|
||
})
|
||
}
|
||
|
||
const submitCounter = async () => {
|
||
const values = await readForm()
|
||
if (!values) return
|
||
await run('counter', async () => {
|
||
await counterCheckoutApi(order.id, {
|
||
content: values.reason ?? '',
|
||
reason: values.reason ?? '',
|
||
consumable_amount_cent: yuanToCent(values.consumableYuan ?? 0),
|
||
coin_consumed_m: toPositiveNumber(values.coinConsumed ?? 0),
|
||
other_amount_cent: yuanToCent(values.otherYuan ?? 0),
|
||
deposit_deduct_amount_cent: yuanToCent(values.otherYuan ?? 0),
|
||
evidence_urls: evidencePayload(values),
|
||
})
|
||
message.success('结账方案已修改,等待对方处理')
|
||
})
|
||
}
|
||
|
||
const submitDispute = async () => {
|
||
const values = await readForm()
|
||
if (!values) return
|
||
const confirmed = await confirmModal(modal, {
|
||
title: actions.checkoutDisputeStage ? '发起结账争议' : '发起申诉',
|
||
content: '提交后订单会进入仲裁流程,客服会根据双方证据处理。确定提交吗?',
|
||
okText: '提交',
|
||
cancelText: '再想想',
|
||
})
|
||
if (!confirmed) return
|
||
await run('dispute', async () => {
|
||
await createOrderDispute(order.id, {
|
||
type: actions.checkoutDisputeStage ? 'checkout_dispute' : (values.disputeType ?? 'other'),
|
||
description: values.content ?? '',
|
||
evidence_urls: evidencePayload(values),
|
||
})
|
||
message.success('申诉已提交')
|
||
})
|
||
}
|
||
|
||
const cancelDispute = async () => {
|
||
const confirmed = await confirmModal(modal, {
|
||
title: '取消申诉',
|
||
content: '取消后订单会恢复到发起申诉前的流程,确定继续吗?',
|
||
okText: '取消申诉',
|
||
cancelText: '保留申诉',
|
||
})
|
||
if (!confirmed) return
|
||
await run('cancel-dispute', async () => {
|
||
await cancelOrderDispute(order.id)
|
||
message.success('申诉已取消')
|
||
})
|
||
}
|
||
|
||
const cancelOrder = async () => {
|
||
const refundReview = order.status === 'pending_handoff'
|
||
const confirmed = await confirmModal(modal, {
|
||
title: refundReview ? '申请取消订单' : '取消订单',
|
||
content: refundReview
|
||
? '订单已支付,取消后需要客服审核退款,审核通过前账号仍会保持占用。'
|
||
: '取消后订单会关闭,已冻结的押金将按原路退回。',
|
||
okText: refundReview ? '申请取消' : '取消订单',
|
||
cancelText: '再想想',
|
||
})
|
||
if (!confirmed) return
|
||
await run('cancel', async () => {
|
||
await cancelMyOrder(order.id)
|
||
message.success(refundReview ? '取消申请已提交' : '订单已取消')
|
||
})
|
||
}
|
||
|
||
const dialogTitle: Record<Exclude<DialogKind, null>, string> = {
|
||
pay: '支付订单',
|
||
handoff: '提交交接说明',
|
||
checkout: '发起结账',
|
||
counter: '修改结账方案',
|
||
dispute: actions.checkoutDisputeStage ? '发起结账争议' : '发起申诉',
|
||
}
|
||
|
||
return (
|
||
<Card title="订单操作" className="pc-order-action-card">
|
||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||
{actions.canPay ? (
|
||
<Space wrap>
|
||
<Button type="primary" onClick={() => openDialog('pay')}>
|
||
去支付
|
||
</Button>
|
||
<Button loading={busy === 'check-pay'} onClick={() => void checkPayment()}>
|
||
检查支付状态
|
||
</Button>
|
||
</Space>
|
||
) : null}
|
||
|
||
{actions.canSubmitHandoff ? (
|
||
<Button type="primary" onClick={() => openDialog('handoff')}>
|
||
提交交接说明
|
||
</Button>
|
||
) : null}
|
||
|
||
{actions.canConfirmReceive ? (
|
||
<Button type="primary" loading={busy === 'receive'} onClick={() => void submitReceive()}>
|
||
确认收号
|
||
</Button>
|
||
) : null}
|
||
|
||
{actions.canSubmitCheckout ? (
|
||
<Button type="primary" onClick={() => openDialog('checkout')}>
|
||
发起结账
|
||
</Button>
|
||
) : null}
|
||
|
||
{actions.canConfirmCheckout ? (
|
||
<Button
|
||
type="primary"
|
||
loading={busy === 'confirm-checkout'}
|
||
onClick={() => void submitConfirmCheckout()}
|
||
>
|
||
确认结账
|
||
</Button>
|
||
) : null}
|
||
|
||
{actions.canAcceptCheckout ? (
|
||
<Button
|
||
type="primary"
|
||
loading={busy === 'accept-checkout'}
|
||
onClick={() => void submitAcceptCheckout()}
|
||
>
|
||
同意结账方案
|
||
</Button>
|
||
) : null}
|
||
|
||
<Space wrap>
|
||
{actions.canCounterCheckout ? (
|
||
<Button onClick={() => openDialog('counter')}>修改结账方案</Button>
|
||
) : null}
|
||
{actions.canOpenDispute ? (
|
||
<Button danger onClick={() => openDialog('dispute')}>
|
||
{actions.checkoutDisputeStage ? '拒绝方案并发起争议' : '发起申诉'}
|
||
</Button>
|
||
) : null}
|
||
{actions.canCancelDispute ? (
|
||
<Button danger loading={busy === 'cancel-dispute'} onClick={() => void cancelDispute()}>
|
||
取消申诉
|
||
</Button>
|
||
) : null}
|
||
{actions.canCancelOrder ? (
|
||
<Button danger loading={busy === 'cancel'} onClick={() => void cancelOrder()}>
|
||
取消订单
|
||
</Button>
|
||
) : null}
|
||
</Space>
|
||
|
||
{!actions.hasAction ? (
|
||
<Typography.Text type="secondary">当前订单没有可执行操作。</Typography.Text>
|
||
) : null}
|
||
|
||
{checkout && checkout.shortfall_cent > 0 ? (
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
message={`押金缺口 ${formatMoney(checkout.shortfall_cent)}`}
|
||
description="押金不足以覆盖当前方案,需修改方案或发起争议由客服处理。"
|
||
/>
|
||
) : null}
|
||
</Space>
|
||
|
||
<Modal
|
||
open={Boolean(dialog)}
|
||
title={dialog ? dialogTitle[dialog] : ''}
|
||
width={640}
|
||
onCancel={closeDialog}
|
||
maskClosable={!busy}
|
||
confirmLoading={Boolean(busy)}
|
||
footer={
|
||
dialog === 'pay' && payURL ? (
|
||
<Space>
|
||
<Button onClick={closeDialog} disabled={Boolean(busy)}>
|
||
关闭
|
||
</Button>
|
||
<Button loading={busy === 'check-pay'} onClick={() => void checkPayment()}>
|
||
检查支付状态
|
||
</Button>
|
||
</Space>
|
||
) : (
|
||
<Space>
|
||
<Button onClick={closeDialog} disabled={Boolean(busy)}>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
loading={Boolean(busy)}
|
||
onClick={() => {
|
||
if (dialog === 'pay') void submitPay()
|
||
if (dialog === 'handoff') void submitHandoff()
|
||
if (dialog === 'checkout') void submitCheckout()
|
||
if (dialog === 'counter') void submitCounter()
|
||
if (dialog === 'dispute') void submitDispute()
|
||
}}
|
||
>
|
||
{dialog === 'pay' ? '发起支付' : dialog === 'handoff' ? '提交交接' : '提交'}
|
||
</Button>
|
||
</Space>
|
||
)
|
||
}
|
||
>
|
||
{dialog === 'pay' ? (
|
||
<>
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="选择支付方式"
|
||
description="支付成功后才进入交接流程;二维码创建后系统会自动检查支付结果。"
|
||
/>
|
||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||
<Form.Item name="payWay" label="支付方式">
|
||
<Radio.Group>
|
||
<Radio.Button value="WXZF">微信支付</Radio.Button>
|
||
<Radio.Button value="ZFBZF">支付宝</Radio.Button>
|
||
</Radio.Group>
|
||
</Form.Item>
|
||
</Form>
|
||
{payURL ? (
|
||
<Alert
|
||
type="success"
|
||
showIcon
|
||
message="支付单已创建"
|
||
description={
|
||
<Space direction="vertical" align="center">
|
||
<QRCode value={payURL} size={180} />
|
||
<span>请使用微信、支付宝或云闪付扫码付款,支付成功后本页会自动刷新。</span>
|
||
<Button type="primary" href={payURL} target="_blank">
|
||
在支付应用中打开
|
||
</Button>
|
||
</Space>
|
||
}
|
||
/>
|
||
) : null}
|
||
</>
|
||
) : null}
|
||
|
||
{dialog === 'handoff' ? (
|
||
<>
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="交接说明会展示给租客"
|
||
description="请写清账号、密码、区服、注意事项与可登录时间。"
|
||
/>
|
||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||
<Form.Item
|
||
name="content"
|
||
label="交接说明"
|
||
rules={[{ required: true, message: '请填写交接说明' }]}
|
||
>
|
||
<Input.TextArea rows={6} maxLength={1000} showCount />
|
||
</Form.Item>
|
||
</Form>
|
||
</>
|
||
) : null}
|
||
|
||
{dialog === 'checkout' || dialog === 'counter' ? (
|
||
<Form form={form} layout="vertical">
|
||
{dialog === 'counter' ? (
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message={`第 ${checkout?.round_count ?? 1}/${checkout?.max_rounds ?? 6} 轮协商`}
|
||
description="修改后由对方继续确认或还价,最多 6 轮。"
|
||
/>
|
||
) : null}
|
||
<Form.Item
|
||
name={dialog === 'counter' ? 'reason' : 'content'}
|
||
label={dialog === 'counter' ? '修改原因' : '结账备注'}
|
||
rules={[{ required: true, message: '请填写说明' }]}
|
||
style={{ marginTop: 16 }}
|
||
>
|
||
<Input.TextArea
|
||
rows={4}
|
||
maxLength={500}
|
||
showCount
|
||
placeholder={
|
||
dialog === 'counter'
|
||
? '说明为什么需要调整金额或扣除押金'
|
||
: '说明账号使用情况、消耗品、哈夫币与费用依据'
|
||
}
|
||
/>
|
||
</Form.Item>
|
||
<Space size={12} style={{ width: '100%' }} align="start">
|
||
<Form.Item name="consumableYuan" label="消耗品金额(元)">
|
||
<InputNumber min={0} precision={2} style={{ width: 160 }} />
|
||
</Form.Item>
|
||
<Form.Item name="coinConsumed" label="哈夫币消耗(M)">
|
||
<InputNumber min={0} precision={2} style={{ width: 160 }} />
|
||
</Form.Item>
|
||
<Form.Item name="otherYuan" label="押金扣除(元)">
|
||
<InputNumber min={0} precision={2} style={{ width: 160 }} />
|
||
</Form.Item>
|
||
</Space>
|
||
<Form.Item name="evidenceUrls" label="证据图片(可选)">
|
||
<EvidenceUpload scene="handoff" max={6} />
|
||
</Form.Item>
|
||
<Form.Item name="evidenceText" label="外部证据链接(每行一条,可选)">
|
||
<Input.TextArea rows={2} placeholder="https://..." />
|
||
</Form.Item>
|
||
</Form>
|
||
) : null}
|
||
|
||
{dialog === 'dispute' ? (
|
||
<Form form={form} layout="vertical">
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
message="提交后进入仲裁"
|
||
description="请写清时间、经过、诉求与已经沟通的结果,并尽量附上证据链接。"
|
||
/>
|
||
{!actions.checkoutDisputeStage ? (
|
||
<Form.Item name="disputeType" label="申诉类型" style={{ marginTop: 16 }}>
|
||
<Select options={DISPUTE_TYPES} />
|
||
</Form.Item>
|
||
) : (
|
||
<Tag color="red" className="pc-order-dispute-tag">
|
||
结账争议
|
||
</Tag>
|
||
)}
|
||
<Form.Item
|
||
name="content"
|
||
label="申诉说明"
|
||
rules={[{ required: true, message: '请填写申诉说明' }]}
|
||
>
|
||
<Input.TextArea rows={5} maxLength={1000} showCount />
|
||
</Form.Item>
|
||
<Form.Item name="evidenceUrls" label="证据图片(可选)">
|
||
<EvidenceUpload scene="dispute" max={6} />
|
||
</Form.Item>
|
||
<Form.Item name="evidenceText" label="外部证据链接(每行一条,可选)">
|
||
<Input.TextArea rows={2} placeholder="https://..." />
|
||
</Form.Item>
|
||
</Form>
|
||
) : null}
|
||
</Modal>
|
||
</Card>
|
||
)
|
||
}
|