add:新增验证视频可选上传功能
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ReloadOutlined } from '@ant-design/icons'
|
||||
import { ReloadOutlined, VideoCameraOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
App,
|
||||
@@ -26,7 +26,12 @@ import {
|
||||
repayAdminAfterSalesCaseDebt,
|
||||
resolveAdminAfterSalesCase,
|
||||
} from '@/services/admin'
|
||||
import type { UploadedFile, WorkerAfterSalesCase, WorkOrder } from '@/types/worker-platform'
|
||||
import type {
|
||||
UploadedFile,
|
||||
WorkerAcceptanceVideo,
|
||||
WorkerAfterSalesCase,
|
||||
WorkOrder,
|
||||
} from '@/types/worker-platform'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { uniqueUploadedFiles } from '@/utils/image-identity'
|
||||
@@ -259,6 +264,15 @@ const AfterSalesPanel = forwardRef<AfterSalesPanelHandle>(function AfterSalesPan
|
||||
/>
|
||||
</Space>
|
||||
) : null}
|
||||
{getAfterSalesSnapshotVideo(item) ? (
|
||||
<a
|
||||
href={getAfterSalesSnapshotVideo(item)?.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<VideoCameraOutlined /> 查看固化验收视频
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -570,4 +584,21 @@ function getAfterSalesSnapshotFiles(item: WorkerAfterSalesCase): UploadedFile[]
|
||||
)
|
||||
}
|
||||
|
||||
/** 售后固化快照里的验收视频(有则给出可点击链接)。 */
|
||||
function getAfterSalesSnapshotVideo(item: WorkerAfterSalesCase): WorkerAcceptanceVideo | null {
|
||||
const snapshot = asRecord(item.acceptanceSnapshot)
|
||||
const acceptance = asRecord(snapshot.acceptance)
|
||||
const video = asRecord(acceptance.video)
|
||||
const url = String(video.url || '').trim()
|
||||
if (!url) return null
|
||||
return {
|
||||
objectKey: String(video.objectKey || video.object_key || ''),
|
||||
url,
|
||||
filename: String(video.filename || ''),
|
||||
contentType: String(video.contentType || video.content_type || ''),
|
||||
size: Number(video.size || 0),
|
||||
durationSeconds: Number(video.durationSeconds || video.duration_seconds || 0),
|
||||
}
|
||||
}
|
||||
|
||||
export default AfterSalesPanel
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
UserAddOutlined,
|
||||
WarningOutlined,
|
||||
RollbackOutlined,
|
||||
VideoCameraOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popover,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
@@ -45,6 +47,7 @@ import JsonPreview from '@/components/admin/JsonPreview'
|
||||
import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import VideoPreview, { formatAcceptanceVideoDuration } from '@/components/files/VideoPreview'
|
||||
import {
|
||||
acceptAdminWorkOrder,
|
||||
acceptAdminWorkOrderShare,
|
||||
@@ -2603,6 +2606,11 @@ export default function WorkOrdersPanel() {
|
||||
{getAcceptanceFiles(detailOrder).length +
|
||||
getAcceptanceImageUrls(detailOrder).length || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="验收视频">
|
||||
{detailOrder.acceptance?.video
|
||||
? formatAcceptanceVideoDuration(detailOrder.acceptance.video.durationSeconds)
|
||||
: '未上传'}
|
||||
</Descriptions.Item>
|
||||
{detailOrder.acceptance?.reviewMode === 'vip_auto' ? (
|
||||
<Descriptions.Item label="VIP 补图">
|
||||
{detailOrder.acceptance.evidenceStatus === 'pending'
|
||||
@@ -2626,6 +2634,11 @@ export default function WorkOrdersPanel() {
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无验收图</Typography.Text>
|
||||
)}
|
||||
<VideoPreview
|
||||
video={detailOrder.acceptance?.video ?? null}
|
||||
maxHeight={200}
|
||||
emptyText="暂无验收视频(选填)"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -3205,7 +3218,7 @@ export default function WorkOrdersPanel() {
|
||||
loading={sharingQuery.isFetching}
|
||||
pagination={false}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: 1528 }}
|
||||
scroll={{ x: 1624 }}
|
||||
rowClassName={(share) =>
|
||||
share.problemMarked
|
||||
? 'admin-share-row-problem'
|
||||
@@ -3316,6 +3329,26 @@ export default function WorkOrdersPanel() {
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '验收视频',
|
||||
width: 96,
|
||||
render: (_, share) => {
|
||||
const video = share.acceptance?.video
|
||||
if (!video?.url) return '-'
|
||||
return (
|
||||
<Popover
|
||||
trigger="click"
|
||||
placement="left"
|
||||
title="验收视频"
|
||||
content={<VideoPreview video={video} maxHeight={240} showMeta />}
|
||||
>
|
||||
<Button size="small" icon={<VideoCameraOutlined />}>
|
||||
查看
|
||||
</Button>
|
||||
</Popover>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
width: 120,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Alert, Button, Card, Descriptions, Drawer, Space, Steps, Tag, Typograph
|
||||
|
||||
import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline'
|
||||
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
||||
import VideoPreview, { formatAcceptanceVideoDuration } from '@/components/files/VideoPreview'
|
||||
import type { WorkOrder } from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { resolveOrderConsignmentScreenshotUrls } from '@/utils/consignment-display'
|
||||
@@ -104,7 +105,7 @@ export function WorkerOrderDetailDrawer({
|
||||
icon={<PictureOutlined />}
|
||||
onClick={() => onOpenAcceptance(order)}
|
||||
>
|
||||
{hasSubmittedAcceptance(order) ? '补充图片' : '上传图片 / 提交验收'}
|
||||
{hasSubmittedAcceptance(order) ? '补充验收资料' : '上传图片/视频 · 提交验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
{canRequestCancel(order) ? (
|
||||
@@ -322,6 +323,7 @@ function GiftProgressCard({ order }: { order: WorkOrder }) {
|
||||
function AcceptanceSummary({ order }: { order: WorkOrder }) {
|
||||
const acceptanceFiles = getUniqueAcceptanceFiles(getWorkerAcceptance(order))
|
||||
const draftFiles = getUniqueAcceptanceFiles(getWorkerDraft(order))
|
||||
const acceptanceVideo = getWorkerAcceptance(order)?.video ?? getWorkerDraft(order)?.video ?? null
|
||||
const hasAcceptance = acceptanceFiles.length > 0
|
||||
const hasDraft = draftFiles.length > 0 && !hasSubmittedAcceptance(order)
|
||||
|
||||
@@ -329,7 +331,12 @@ function AcceptanceSummary({ order }: { order: WorkOrder }) {
|
||||
<div className="worker-order-detail-stack">
|
||||
<Descriptions column={{ xs: 1, md: 2 }} bordered size="small">
|
||||
<Descriptions.Item label="验收图片数量">{acceptanceFiles.length}</Descriptions.Item>
|
||||
<Descriptions.Item label="提交时间">
|
||||
<Descriptions.Item label="验收视频">
|
||||
{acceptanceVideo
|
||||
? formatAcceptanceVideoDuration(acceptanceVideo.durationSeconds)
|
||||
: '未上传'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="提交时间" span={2}>
|
||||
{formatDateTime(getAcceptanceSubmittedAt(order))}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="验收说明" span={2}>
|
||||
@@ -345,6 +352,8 @@ function AcceptanceSummary({ order }: { order: WorkOrder }) {
|
||||
<Typography.Text type="secondary">暂无已提交验收图片</Typography.Text>
|
||||
)}
|
||||
|
||||
<VideoPreview video={acceptanceVideo} maxHeight={180} emptyText="暂无验收视频(选填)" />
|
||||
|
||||
{hasDraft ? (
|
||||
<div className="acceptance-draft-badge-wrap" style={{ marginTop: 8 }}>
|
||||
<Tag color="processing" icon={<PictureOutlined />}>
|
||||
|
||||
@@ -476,7 +476,7 @@ function WorkerOrderMobileCard({
|
||||
className="worker-order-mobile-action-submit"
|
||||
onClick={() => onOpenAcceptance(order)}
|
||||
>
|
||||
{hasSubmittedAcceptance(order) ? '补充图片' : '提交验收'}
|
||||
{hasSubmittedAcceptance(order) ? '补充验收资料' : '上传图片/视频 · 提交验收'}
|
||||
</Button>
|
||||
) : null}
|
||||
{canRemindAcceptance(order) ? (
|
||||
|
||||
@@ -3,8 +3,10 @@ import { Button, Form, Input, Modal, Radio, Space, Tag, Typography } from 'antd'
|
||||
import type { FormInstance } from 'antd'
|
||||
|
||||
import ImageUpload from '@/components/files/ImageUpload'
|
||||
import VideoUpload from '@/components/files/VideoUpload'
|
||||
import type {
|
||||
UploadedFile,
|
||||
WorkerAcceptanceVideo,
|
||||
WorkerCancelRequestRisk,
|
||||
WorkerOrderFeedback,
|
||||
WorkOrder,
|
||||
@@ -59,10 +61,12 @@ export function WorkerOrderModals({
|
||||
onSaveNote,
|
||||
editingOrder,
|
||||
acceptanceFiles,
|
||||
acceptanceVideo,
|
||||
acceptanceForm,
|
||||
workerCanAutoAcceptWithoutEvidence,
|
||||
onCloseAcceptance,
|
||||
onAcceptanceFilesChange,
|
||||
onAcceptanceVideoChange,
|
||||
onSubmitAcceptance,
|
||||
onSaveAcceptanceImages,
|
||||
giftMaterialOrder,
|
||||
@@ -89,10 +93,12 @@ export function WorkerOrderModals({
|
||||
onSaveNote: (values: WorkerNoteFormValues) => void | Promise<void>
|
||||
editingOrder: WorkOrder | null
|
||||
acceptanceFiles: UploadedFile[]
|
||||
acceptanceVideo: WorkerAcceptanceVideo | null
|
||||
acceptanceForm: FormInstance<AcceptanceFormValues>
|
||||
workerCanAutoAcceptWithoutEvidence: boolean
|
||||
onCloseAcceptance: () => void
|
||||
onAcceptanceFilesChange: (files: UploadedFile[]) => void
|
||||
onAcceptanceVideoChange: (video: WorkerAcceptanceVideo | null) => void
|
||||
onSubmitAcceptance: (values: AcceptanceFormValues) => void | Promise<void>
|
||||
onSaveAcceptanceImages: (values: AcceptanceFormValues) => void | Promise<void>
|
||||
giftMaterialOrder: WorkOrder | null
|
||||
@@ -283,6 +289,12 @@ export function WorkerOrderModals({
|
||||
showPasteHint
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="验收视频(选填)"
|
||||
extra="选片后会在本机压到 720p 再上传;已提交的验收只能替换视频,不能删除。"
|
||||
>
|
||||
<VideoUpload value={acceptanceVideo} onChange={onAcceptanceVideoChange} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
submitWorkerOrderFeedback,
|
||||
supplementWorkerAcceptedOrderEvidence,
|
||||
} from '@/services/worker'
|
||||
import type { UploadedFile, WorkOrder } from '@/types/worker-platform'
|
||||
import type { UploadedFile, WorkOrder, WorkerAcceptanceVideo } from '@/types/worker-platform'
|
||||
import { isExplicitOrderNoSearch, normalizeSearchText } from '@/utils/admin-realtime-work-orders'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { useIsMobile } from '@/utils/use-is-mobile'
|
||||
@@ -181,6 +181,9 @@ export default function WorkerOrdersPage() {
|
||||
const [cancelRequestOrder, setCancelRequestOrder] = useState<WorkOrder | null>(null)
|
||||
const [feedbackOrder, setFeedbackOrder] = useState<WorkOrder | null>(null)
|
||||
const [acceptanceFiles, setAcceptanceFiles] = useState<UploadedFile[]>([])
|
||||
/** 验收视频(单条,非必填);dirty 表示本次弹窗里换过视频,用于补图时允许"只补视频" */
|
||||
const [acceptanceVideo, setAcceptanceVideo] = useState<WorkerAcceptanceVideo | null>(null)
|
||||
const [acceptanceVideoDirty, setAcceptanceVideoDirty] = useState(false)
|
||||
const [giftMaterialOrder, setGiftMaterialOrder] = useState<WorkOrder | null>(null)
|
||||
const [giftMaterialFiles, setGiftMaterialFiles] = useState<UploadedFile[]>([])
|
||||
const [form] = Form.useForm<AcceptanceFormValues>()
|
||||
@@ -340,18 +343,20 @@ export default function WorkerOrdersPage() {
|
||||
const editingTarget = buildShareEditingOrder(order, shareId)
|
||||
setEditingShareId(shareId)
|
||||
setEditingOrder(editingTarget)
|
||||
setAcceptanceVideoDirty(false)
|
||||
const submitted = getWorkerAcceptance(editingTarget)
|
||||
if (canSupplementAcceptedEvidence(editingTarget)) {
|
||||
// 已验收补图只上传新增文件,原有验收图片由服务端保留。
|
||||
// 已验收补图只上传新增文件,原有验收图片由服务端保留;视频只能替换、不能删除。
|
||||
setAcceptanceFiles([])
|
||||
setAcceptanceVideo(submitted?.video ?? null)
|
||||
form.resetFields()
|
||||
setDetailOrder(null)
|
||||
return
|
||||
}
|
||||
// 已提交:以已提交的验收资料为初始内容;未提交:以暂存草稿为初始内容
|
||||
const source = hasSubmittedAcceptance(editingTarget)
|
||||
? getWorkerAcceptance(editingTarget)
|
||||
: getWorkerDraft(editingTarget)
|
||||
const source = hasSubmittedAcceptance(editingTarget) ? submitted : getWorkerDraft(editingTarget)
|
||||
setAcceptanceFiles(getUniqueAcceptanceFiles(source))
|
||||
setAcceptanceVideo(source?.video ?? null)
|
||||
form.setFieldsValue({ note: String(source?.note || '') })
|
||||
setDetailOrder(null)
|
||||
}
|
||||
@@ -484,22 +489,25 @@ export default function WorkerOrdersPage() {
|
||||
if (!editingOrder) return
|
||||
const isAcceptedEvidenceSupplement = canSupplementAcceptedEvidence(editingOrder)
|
||||
if (isAcceptedEvidenceSupplement) {
|
||||
if (acceptanceFiles.length === 0) {
|
||||
message.error('请上传新的验收图片')
|
||||
if (acceptanceFiles.length === 0 && !acceptanceVideoDirty) {
|
||||
message.error('请上传新的验收图片,或替换验收视频')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await supplementWorkerAcceptedOrderEvidence(editingOrder.workOrderId, {
|
||||
files: acceptanceFiles,
|
||||
...(acceptanceVideoDirty ? { video: acceptanceVideo } : {}),
|
||||
...(editingShareId ? { shareId: editingShareId } : {}),
|
||||
})
|
||||
message.success('验收图片已补充')
|
||||
message.success('验收资料已补充')
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
setAcceptanceVideo(null)
|
||||
setAcceptanceVideoDirty(false)
|
||||
form.resetFields()
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '补充验收图片失败')
|
||||
message.error(error instanceof Error ? error.message : '补充验收资料失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -518,11 +526,14 @@ export default function WorkerOrdersPage() {
|
||||
await submitWorkerAcceptance(editingOrder.workOrderId, {
|
||||
note: values.note,
|
||||
files: acceptanceFiles,
|
||||
video: acceptanceVideo,
|
||||
...(editingShareId ? { shareId: editingShareId } : {}),
|
||||
})
|
||||
message.success(canAutoAccept ? '已自动验收,可在 24 小时内补充图片' : '已提交验收')
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
setAcceptanceVideo(null)
|
||||
setAcceptanceVideoDirty(false)
|
||||
form.resetFields()
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
@@ -540,13 +551,16 @@ export default function WorkerOrdersPage() {
|
||||
await saveWorkerAcceptanceDraft(editingOrder.workOrderId, {
|
||||
note: values.note,
|
||||
files: acceptanceFiles,
|
||||
video: acceptanceVideo,
|
||||
...(editingShareId ? { shareId: editingShareId } : {}),
|
||||
})
|
||||
message.success(
|
||||
hasSubmittedAcceptance(editingOrder) ? '验收图片已更新' : '验收图片已暂存,可继续补充',
|
||||
hasSubmittedAcceptance(editingOrder) ? '验收资料已更新' : '验收资料已暂存,可继续补充',
|
||||
)
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
setAcceptanceVideo(null)
|
||||
setAcceptanceVideoDirty(false)
|
||||
form.resetFields()
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
@@ -754,14 +768,21 @@ export default function WorkerOrdersPage() {
|
||||
onSaveNote={saveOrderNote}
|
||||
editingOrder={editingOrder}
|
||||
acceptanceFiles={acceptanceFiles}
|
||||
acceptanceVideo={acceptanceVideo}
|
||||
acceptanceForm={form}
|
||||
workerCanAutoAcceptWithoutEvidence={workerCanAutoAcceptWithoutEvidence}
|
||||
onCloseAcceptance={() => {
|
||||
setEditingOrder(null)
|
||||
setAcceptanceFiles([])
|
||||
setAcceptanceVideo(null)
|
||||
setAcceptanceVideoDirty(false)
|
||||
form.resetFields()
|
||||
}}
|
||||
onAcceptanceFilesChange={setAcceptanceFiles}
|
||||
onAcceptanceVideoChange={(video) => {
|
||||
setAcceptanceVideo(video)
|
||||
setAcceptanceVideoDirty(true)
|
||||
}}
|
||||
onSubmitAcceptance={submitAcceptance}
|
||||
onSaveAcceptanceImages={saveAcceptanceImages}
|
||||
giftMaterialOrder={giftMaterialOrder}
|
||||
|
||||
Reference in New Issue
Block a user