253 lines
7.8 KiB
TypeScript
253 lines
7.8 KiB
TypeScript
import { Descriptions, Space, Tag, Typography } from 'antd'
|
|
import ImagePreviewList from '@/components/files/ImagePreviewList'
|
|
import { DeadlineCountdown, formatTimeoutPolicyLabel } from '@/components/DeadlineCountdown'
|
|
import type { CollectField, UploadedFile, WorkOrder } from '@/types/worker-platform'
|
|
|
|
export { DeadlineCountdown, formatTimeoutPolicyLabel }
|
|
|
|
type MappedRequirementField = {
|
|
key: string
|
|
label: string
|
|
required: boolean
|
|
type: 'text' | 'select'
|
|
options: string[]
|
|
}
|
|
|
|
export function getRequirementFields(order: WorkOrder | null): CollectField[] {
|
|
if (!order) return []
|
|
const rawFields = Array.isArray(order.requirement?.fields) ? order.requirement.fields : []
|
|
return rawFields
|
|
.map((item) => {
|
|
const source = asRecord(item)
|
|
const key = String(source.key || '').trim()
|
|
if (!key) return null
|
|
return {
|
|
key,
|
|
label: String(source.label || key).trim(),
|
|
required: source.required !== false,
|
|
type: source.type === 'select' ? ('select' as const) : ('text' as const),
|
|
options: Array.isArray(source.options)
|
|
? source.options.map((option) => String(option || '').trim()).filter(Boolean)
|
|
: [],
|
|
}
|
|
})
|
|
.filter((item): item is MappedRequirementField => Boolean(item))
|
|
}
|
|
|
|
export type DetailFieldItem = {
|
|
key: string
|
|
label: string
|
|
required: boolean
|
|
value: string
|
|
}
|
|
|
|
export function getMaterialFields(order: WorkOrder): Record<string, string> {
|
|
const material = asRecord(order.material)
|
|
const collect = asRecord(material.collect)
|
|
const fields = asRecord(collect.fields || material.fields)
|
|
return Object.fromEntries(
|
|
Object.entries(fields).map(([key, value]) => [key, String(value || '')]),
|
|
)
|
|
}
|
|
|
|
export function getMaterialDetailItems(order: WorkOrder): DetailFieldItem[] {
|
|
const values = getMaterialFields(order)
|
|
const requiredFields = getRequirementFields(order)
|
|
const usedKeys = new Set<string>()
|
|
const items = requiredFields.map((field) => {
|
|
usedKeys.add(field.key)
|
|
return {
|
|
key: field.key,
|
|
label: field.label,
|
|
required: field.required,
|
|
value: String(values[field.key] || ''),
|
|
}
|
|
})
|
|
|
|
Object.entries(values).forEach(([key, value]) => {
|
|
if (usedKeys.has(key)) return
|
|
items.push({
|
|
key,
|
|
label: key,
|
|
required: false,
|
|
value: String(value || ''),
|
|
})
|
|
})
|
|
|
|
return items
|
|
}
|
|
|
|
export function renderDetailFieldValues(items: DetailFieldItem[], emptyText: string) {
|
|
if (items.length === 0) {
|
|
return <Typography.Text type="secondary">{emptyText}</Typography.Text>
|
|
}
|
|
|
|
return (
|
|
<Descriptions column={1} bordered size="small">
|
|
{items.map((item) => (
|
|
<Descriptions.Item
|
|
key={item.key}
|
|
label={
|
|
<Space size={6} wrap>
|
|
<span>{item.label}</span>
|
|
{item.required ? <Tag color="orange">必填</Tag> : null}
|
|
</Space>
|
|
}
|
|
>
|
|
<span className="worker-order-detail-value">
|
|
{String(item.value || '').trim() || '-'}
|
|
</span>
|
|
</Descriptions.Item>
|
|
))}
|
|
</Descriptions>
|
|
)
|
|
}
|
|
|
|
export function renderRequirementFieldValues(order: WorkOrder) {
|
|
const fields = getRequirementFields(order)
|
|
if (fields.length === 0) {
|
|
return <Typography.Text type="secondary">当前工单未配置资料要求</Typography.Text>
|
|
}
|
|
|
|
return (
|
|
<Descriptions column={1} bordered size="small">
|
|
{fields.map((field) => (
|
|
<Descriptions.Item key={field.key} label={field.label}>
|
|
<Space size={6} wrap>
|
|
<Typography.Text code>{field.key}</Typography.Text>
|
|
<Tag color={field.required ? 'orange' : 'default'}>
|
|
{field.required ? '必填' : '选填'}
|
|
</Tag>
|
|
</Space>
|
|
</Descriptions.Item>
|
|
))}
|
|
</Descriptions>
|
|
)
|
|
}
|
|
|
|
export function renderMaterialSummary(order: WorkOrder) {
|
|
const fieldLabelMap = new Map(
|
|
getRequirementFields(order).map((field) => [field.key, field.label]),
|
|
)
|
|
const fields = getMaterialFields(order)
|
|
const entries = Object.entries(fields).filter(([, value]) => String(value || '').trim())
|
|
const screenshots = getMaterialScreenshots(order)
|
|
if (entries.length === 0 && screenshots.length === 0) {
|
|
return <Typography.Text type="secondary">-</Typography.Text>
|
|
}
|
|
|
|
return (
|
|
<div className="cell-stack">
|
|
{entries.map(([key, value]) => (
|
|
<Typography.Text key={key} style={{ fontSize: 12 }} ellipsis>
|
|
{fieldLabelMap.get(key) || key}: {value}
|
|
</Typography.Text>
|
|
))}
|
|
{screenshots.length > 0 ? <ImagePreviewList files={screenshots} size={36} /> : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function getAcceptanceFiles(row: WorkOrder): UploadedFile[] {
|
|
return Array.isArray(row.acceptance?.files) ? row.acceptance.files : []
|
|
}
|
|
|
|
export function getAcceptanceImageUrls(row: WorkOrder): string[] {
|
|
return Array.isArray(row.acceptance?.imageUrls)
|
|
? row.acceptance.imageUrls.map((item) => String(item || '').trim()).filter(Boolean)
|
|
: []
|
|
}
|
|
|
|
export function getAcceptanceSubmittedAt(order: WorkOrder): string | null {
|
|
const acceptanceSubmittedAt =
|
|
typeof order.acceptance?.submittedAt === 'string' ? order.acceptance.submittedAt : null
|
|
return order.submittedAt || acceptanceSubmittedAt || null
|
|
}
|
|
|
|
export function getMaterialScreenshots(order: WorkOrder): UploadedFile[] {
|
|
const material = asRecord(order.material)
|
|
const collect = asRecord(material.collect)
|
|
const screenshots = Array.isArray(collect.screenshots) ? collect.screenshots : []
|
|
return screenshots
|
|
.map((item) => {
|
|
const file = asRecord(item)
|
|
const url = String(file.url || '').trim()
|
|
if (!url) return null
|
|
return {
|
|
objectKey: String(file.objectKey || file.object_key || '').trim(),
|
|
filename: String(file.originalFilename || file.filename || '').trim(),
|
|
url,
|
|
mediumUrl: String(file.mediumUrl || file.medium_url || '').trim(),
|
|
thumbnailUrl: String(file.thumbnailUrl || file.thumbnail_url || '').trim(),
|
|
} as UploadedFile
|
|
})
|
|
.filter((item): item is UploadedFile => Boolean(item))
|
|
}
|
|
|
|
export function asRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: {}
|
|
}
|
|
|
|
export function formatMoney(value: number | undefined) {
|
|
return `¥${((Number(value || 0) || 0) / 100).toFixed(2)}`
|
|
}
|
|
|
|
export function formatStatus(status: string) {
|
|
const labels: Record<string, string> = {
|
|
pending_material: '待完善',
|
|
unassigned: '未分配',
|
|
open: '待抢单',
|
|
in_progress: '代练中',
|
|
pending_acceptance: '待验收',
|
|
problem: '问题单',
|
|
vip_evidence_pending: '待补图',
|
|
accepted: '已验收',
|
|
cancelled: '已取消',
|
|
}
|
|
return labels[status] || status
|
|
}
|
|
|
|
export function resolveStatusColor(status: string) {
|
|
const colors: Record<string, string> = {
|
|
pending_material: 'orange',
|
|
unassigned: 'cyan',
|
|
open: 'blue',
|
|
in_progress: 'geekblue',
|
|
pending_acceptance: 'purple',
|
|
problem: 'red',
|
|
vip_evidence_pending: 'gold',
|
|
accepted: 'green',
|
|
cancelled: 'default',
|
|
}
|
|
return colors[status] || 'default'
|
|
}
|
|
|
|
export function formatWorkerStatus(status: string) {
|
|
const labels: Record<string, string> = {
|
|
pending_review: '待处理',
|
|
active: '已启用',
|
|
rejected: '已冻结',
|
|
disabled: '已停用',
|
|
}
|
|
return labels[status] || status
|
|
}
|
|
|
|
export function formatSharingShareStatus(status: string) {
|
|
const labels: Record<string, string> = {
|
|
joined: '已参与',
|
|
submitted: '已提交验收',
|
|
accepted: '已验收',
|
|
cancelled: '已取消',
|
|
}
|
|
return labels[status] || status
|
|
}
|
|
|
|
export function resolveSharingShareStatusColor(status: string) {
|
|
if (status === 'accepted') return 'green'
|
|
if (status === 'submitted') return 'gold'
|
|
if (status === 'cancelled') return 'default'
|
|
return 'purple'
|
|
}
|