feat(order-service): 增加提交资料确认与核对摘要

This commit is contained in:
yml2213
2026-11-04 12:16:39 +08:00
parent d0e0d32ff3
commit e17f336e1c
12 changed files with 378 additions and 225 deletions
@@ -0,0 +1,9 @@
-- 134_order_service_material_confirmation.sql
-- 客户提交资料前的确认弹窗配置;同时移除已废弃的页面公告字段。
ALTER TABLE order_service_profiles
ADD COLUMN IF NOT EXISTS material_confirmation_json JSONB NOT NULL DEFAULT '{}'::jsonb,
DROP COLUMN IF EXISTS announcement_json;
COMMENT ON COLUMN order_service_profiles.material_confirmation_json IS
'提交资料确认:enabled/title/content/confirmText/cancelText';
@@ -12,7 +12,7 @@ export type OrderServiceProfileRow = {
fields_json: Array<Record<string, unknown>>
screenshots_json: Record<string, unknown>
auto_publish_json: Record<string, unknown>
announcement_json: Record<string, unknown>
material_confirmation_json: Record<string, unknown>
submit_text: string
success_text: string
status: string
@@ -99,7 +99,7 @@ export async function upsertOrderServiceProfile(input: {
fields: Array<Record<string, unknown>>
screenshots: Record<string, unknown>
autoPublish: Record<string, unknown>
announcement: Record<string, unknown>
materialConfirmation: Record<string, unknown>
submitText: string
successText: string
adminUserId: number | null
@@ -108,7 +108,8 @@ export async function upsertOrderServiceProfile(input: {
`
INSERT INTO order_service_profiles (
category_id, name, title, description,
guide_image_json, fields_json, screenshots_json, auto_publish_json, announcement_json,
guide_image_json, fields_json, screenshots_json, auto_publish_json,
material_confirmation_json,
submit_text, success_text, status, created_by_admin_user_id
) VALUES (
$1, $2, $3, $4,
@@ -123,7 +124,7 @@ export async function upsertOrderServiceProfile(input: {
fields_json = EXCLUDED.fields_json,
screenshots_json = EXCLUDED.screenshots_json,
auto_publish_json = EXCLUDED.auto_publish_json,
announcement_json = EXCLUDED.announcement_json,
material_confirmation_json = EXCLUDED.material_confirmation_json,
submit_text = EXCLUDED.submit_text,
success_text = EXCLUDED.success_text,
current_version_id = CASE
@@ -143,7 +144,7 @@ export async function upsertOrderServiceProfile(input: {
JSON.stringify(input.fields),
JSON.stringify(input.screenshots),
JSON.stringify(input.autoPublish),
JSON.stringify(input.announcement),
JSON.stringify(input.materialConfirmation),
input.submitText,
input.successText,
input.adminUserId,
@@ -78,41 +78,33 @@ test('订单服务配置规范化:未知字段类型降级为文本,空下
assert.equal(normalized.fields[0]?.type, 'text')
})
test('订单服务页面公告:旧配置默认展示资料填写错误免责声明', () => {
test('订单服务提交确认:旧配置默认展示不可修改与资料错误免责声明', () => {
const normalized = config()
assert.deepEqual(normalized.announcement, {
assert.deepEqual(normalized.materialConfirmation, {
enabled: true,
title: '公告',
content: '由于个人资料填写错误,导致的赠送错误,店铺概不负责!',
fontSize: 14,
color: '#8a4b00',
backgroundColor: '#fff7e6',
fontWeight: 'bold',
textAlign: 'left',
title: '提交资料确认',
content:
'请确认资料填写准确无误。资料提交后不能修改,因个人资料填写错误导致的赠送错误,店铺概不负责!',
confirmText: '确认提交',
cancelText: '返回检查',
})
})
test('订单服务页面公告:支持自定义样式并拦截非法颜色与越界字号', () => {
test('订单服务提交确认:支持自定义标题、正文和按钮文字', () => {
const normalized = config({
announcement: {
materialConfirmation: {
enabled: false,
title: '填写前必看',
content: '请确认资料无误后再提交。',
fontSize: 99,
color: 'red;position:fixed',
backgroundColor: '#123456',
fontWeight: 'normal',
textAlign: 'center',
title: '请再次核对',
content: '提交后无法修改,请返回检查。',
confirmText: '确认无误',
cancelText: '再检查一下',
},
})
assert.deepEqual(normalized.announcement, {
assert.deepEqual(normalized.materialConfirmation, {
enabled: false,
title: '填写前必看',
content: '请确认资料无误后再提交。',
fontSize: 24,
color: '#8a4b00',
backgroundColor: '#123456',
fontWeight: 'normal',
textAlign: 'center',
title: '请再次核对',
content: '提交后无法修改,请返回检查。',
confirmText: '确认无误',
cancelText: '再检查一下',
})
})
@@ -47,15 +47,12 @@ export type OrderServiceAutoPublish = {
retryMax: number
}
export type OrderServiceAnnouncement = {
export type OrderServiceMaterialConfirmation = {
enabled: boolean
title: string
content: string
fontSize: number
color: string
backgroundColor: string
fontWeight: 'normal' | 'bold'
textAlign: 'left' | 'center'
confirmText: string
cancelText: string
}
export type OrderServiceProfileConfig = {
@@ -66,7 +63,7 @@ export type OrderServiceProfileConfig = {
fields: OrderServiceField[]
screenshots: OrderServiceScreenshots
autoPublish: OrderServiceAutoPublish
announcement: OrderServiceAnnouncement
materialConfirmation: OrderServiceMaterialConfirmation
submitText: string
successText: string
}
@@ -88,15 +85,13 @@ const FIELD_TYPES = new Set<OrderServiceFieldType>([
'date',
])
export const DEFAULT_ORDER_SERVICE_ANNOUNCEMENT: OrderServiceAnnouncement = {
export const DEFAULT_ORDER_SERVICE_MATERIAL_CONFIRMATION: OrderServiceMaterialConfirmation = {
enabled: true,
title: '公告',
content: '由于个人资料填写错误,导致的赠送错误,店铺概不负责!',
fontSize: 14,
color: '#8a4b00',
backgroundColor: '#fff7e6',
fontWeight: 'bold',
textAlign: 'left',
title: '提交资料确认',
content:
'请确认资料填写准确无误。资料提交后不能修改,因个人资料填写错误导致的赠送错误,店铺概不负责!',
confirmText: '确认提交',
cancelText: '返回检查',
}
export async function listAdminOrderServiceProfiles() {
@@ -138,7 +133,9 @@ export async function saveAdminOrderServiceProfile(payload: JsonObject = {}) {
const fields = normalizeFields(payload.fields ?? payload.fields_json)
const screenshots = normalizeScreenshots(payload.screenshots ?? payload.screenshots_json)
const autoPublish = normalizeAutoPublish(payload.autoPublish ?? payload.auto_publish_json)
const announcement = normalizeAnnouncement(payload.announcement ?? payload.announcement_json)
const materialConfirmation = normalizeMaterialConfirmation(
payload.materialConfirmation ?? payload.material_confirmation_json,
)
const guideImage = normalizeGuideImage(payload.guideImage ?? payload.guide_image_json)
const adminUserId = Number(payload.adminUserId ?? payload.admin_user_id ?? 0) || null
@@ -151,7 +148,7 @@ export async function saveAdminOrderServiceProfile(payload: JsonObject = {}) {
fields,
screenshots,
autoPublish,
announcement,
materialConfirmation,
submitText: text(payload.submitText, 30) || '提交资料',
successText: text(payload.successText, 80) || '资料已提交,请等待处理',
adminUserId,
@@ -229,7 +226,7 @@ function mapAdminProfile(row: OrderServiceProfileRow) {
fields: row.fields_json,
screenshots: row.screenshots_json,
autoPublish: row.auto_publish_json,
announcement: normalizeAnnouncement(row.announcement_json),
materialConfirmation: normalizeMaterialConfirmation(row.material_confirmation_json),
submitText: row.submit_text,
successText: row.success_text,
status: row.status,
@@ -248,7 +245,7 @@ export function buildProfileConfig(row: {
fields_json: unknown
screenshots_json: unknown
auto_publish_json: unknown
announcement_json: unknown
material_confirmation_json: unknown
submit_text: string
success_text: string
}): OrderServiceProfileConfig & { guideImage: Record<string, unknown> } {
@@ -260,7 +257,7 @@ export function buildProfileConfig(row: {
fields: normalizeFields(row.fields_json),
screenshots: normalizeScreenshots(row.screenshots_json),
autoPublish: normalizeAutoPublish(row.auto_publish_json),
announcement: normalizeAnnouncement(row.announcement_json),
materialConfirmation: normalizeMaterialConfirmation(row.material_confirmation_json),
submitText: row.submit_text || '提交资料',
successText: row.success_text || '资料已提交,请等待处理',
}
@@ -312,7 +309,7 @@ export function normalizeProfileConfig(value: JsonObject): OrderServiceProfileCo
fields: normalizeFields(value.fields),
screenshots: normalizeScreenshots(value.screenshots),
autoPublish: normalizeAutoPublish(value.autoPublish),
announcement: normalizeAnnouncement(value.announcement),
materialConfirmation: normalizeMaterialConfirmation(value.materialConfirmation),
submitText: text(value.submitText, 30) || '提交资料',
successText: text(value.successText, 80) || '资料已提交,请等待处理',
}
@@ -391,25 +388,18 @@ function normalizeAutoPublish(value: unknown): OrderServiceAutoPublish {
}
}
function normalizeAnnouncement(value: unknown): OrderServiceAnnouncement {
function normalizeMaterialConfirmation(value: unknown): OrderServiceMaterialConfirmation {
const source = value && typeof value === 'object' ? (value as JsonObject) : {}
return {
enabled: source.enabled !== false,
title: text(source.title, 30) || DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.title,
content: text(source.content, 500) || DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.content,
fontSize: clampInteger(source.fontSize ?? source.font_size, 14, 12, 24),
color: normalizeHexColor(source.color, DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.color),
backgroundColor: normalizeHexColor(
source.backgroundColor ?? source.background_color,
DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.backgroundColor,
),
fontWeight:
source.fontWeight === 'normal' || source.font_weight === 'normal'
? 'normal'
: source.fontWeight === 'bold' || source.font_weight === 'bold'
? 'bold'
: DEFAULT_ORDER_SERVICE_ANNOUNCEMENT.fontWeight,
textAlign: source.textAlign === 'center' || source.text_align === 'center' ? 'center' : 'left',
title: text(source.title, 40) || DEFAULT_ORDER_SERVICE_MATERIAL_CONFIRMATION.title,
content: text(source.content, 500) || DEFAULT_ORDER_SERVICE_MATERIAL_CONFIRMATION.content,
confirmText:
text(source.confirmText ?? source.confirm_text, 12) ||
DEFAULT_ORDER_SERVICE_MATERIAL_CONFIRMATION.confirmText,
cancelText:
text(source.cancelText ?? source.cancel_text, 12) ||
DEFAULT_ORDER_SERVICE_MATERIAL_CONFIRMATION.cancelText,
}
}
@@ -418,11 +408,6 @@ function normalizeGuideImage(value: unknown): Record<string, unknown> {
return normalized ? { ...normalized } : {}
}
function normalizeHexColor(value: unknown, fallback: string) {
const normalized = text(value, 7)
return /^#[0-9a-fA-F]{6}$/.test(normalized) ? normalized.toLowerCase() : fallback
}
function clampInteger(value: unknown, fallback: number, min: number, max: number) {
const parsed = Number(value)
if (!Number.isFinite(parsed)) return fallback
@@ -31,7 +31,7 @@ import {
throwPublicMaterialBlocked,
} from './work-order-material-link-service.js'
import {
DEFAULT_ORDER_SERVICE_ANNOUNCEMENT,
DEFAULT_ORDER_SERVICE_MATERIAL_CONFIRMATION,
getOrderServiceProfileSnapshotByVersion,
normalizeProfileConfig,
type OrderServiceField,
@@ -78,7 +78,7 @@ export type OrderServicePublicItem = {
message: string
canSubmit: boolean
blockedReason: string
announcement?: OrderServiceProfileConfig['announcement']
materialConfirmation?: OrderServiceProfileConfig['materialConfirmation']
fields?: OrderServiceField[]
values?: Record<string, string>
screenshots?: Array<Record<string, unknown>>
@@ -371,7 +371,9 @@ async function mapPublicOrderItem(order: WorkOrderRow): Promise<OrderServicePubl
}),
canSubmit: state.canSubmit,
blockedReason: state.blockedReason,
...(profile?.announcement ? { announcement: profile.announcement } : {}),
...(profile?.materialConfirmation
? { materialConfirmation: profile.materialConfirmation }
: {}),
}
if (!state.canSubmit) return base
@@ -429,7 +431,7 @@ async function resolveProfileConfig(
helpText: '',
},
autoPublish: { enabled: false, requireVoucherConsume: true, retryMax: 3 },
announcement: DEFAULT_ORDER_SERVICE_ANNOUNCEMENT,
materialConfirmation: DEFAULT_ORDER_SERVICE_MATERIAL_CONFIRMATION,
submitText: '提交资料',
successText: '资料已提交,请等待处理',
}
@@ -40,8 +40,8 @@ import {
saveAdminOrderServiceProfile,
type AdminOrderServiceAutoPublishJob,
type AdminOrderServiceProfile,
type OrderServiceAnnouncement,
type OrderServiceField,
type OrderServiceMaterialConfirmation,
} from '@/services/admin/worker-platform'
import { uploadFile } from '@/services/files'
import type { UploadedFile } from '@/types/worker-platform'
@@ -72,7 +72,7 @@ type OrderServiceFormValues = {
successText: string
screenshots: ScreenshotForm
autoPublish: AutoPublishForm
announcement: OrderServiceAnnouncement
materialConfirmation: OrderServiceMaterialConfirmation
}
const DEFAULT_SCREENSHOTS: ScreenshotForm = {
@@ -90,15 +90,13 @@ const DEFAULT_AUTO_PUBLISH: AutoPublishForm = {
retryMax: 3,
}
const DEFAULT_ANNOUNCEMENT: OrderServiceAnnouncement = {
const DEFAULT_MATERIAL_CONFIRMATION: OrderServiceMaterialConfirmation = {
enabled: true,
title: '公告',
content: '由于个人资料填写错误,导致的赠送错误,店铺概不负责!',
fontSize: 14,
color: '#8a4b00',
backgroundColor: '#fff7e6',
fontWeight: 'bold',
textAlign: 'left',
title: '提交资料确认',
content:
'请确认资料填写准确无误。资料提交后不能修改,因个人资料填写错误导致的赠送错误,店铺概不负责!',
confirmText: '确认提交',
cancelText: '返回检查',
}
export default function OrderServicePanel() {
@@ -154,7 +152,8 @@ export default function OrderServicePanel() {
(file: File, scene: string) => uploadFile(file, scene, 'admin'),
[],
)
const announcementPreview = Form.useWatch('announcement', form) || DEFAULT_ANNOUNCEMENT
const materialConfirmationPreview =
Form.useWatch('materialConfirmation', form) || DEFAULT_MATERIAL_CONFIRMATION
useEffect(() => {
if (selectedCategoryId !== null || categories.length === 0) return
@@ -174,7 +173,7 @@ export default function OrderServicePanel() {
successText: nextProfile?.successText || '资料已提交,请等待处理',
screenshots: nextProfile?.screenshots || DEFAULT_SCREENSHOTS,
autoPublish: nextProfile?.autoPublish || DEFAULT_AUTO_PUBLISH,
announcement: nextProfile?.announcement || DEFAULT_ANNOUNCEMENT,
materialConfirmation: nextProfile?.materialConfirmation || DEFAULT_MATERIAL_CONFIRMATION,
})
setFields(normalizeFieldOrder(nextProfile?.fields || []))
setGuideImage(nextProfile?.guideImage || null)
@@ -247,7 +246,7 @@ export default function OrderServicePanel() {
fields: normalizedFields,
screenshots: values.screenshots,
autoPublish: values.autoPublish,
announcement: values.announcement,
materialConfirmation: values.materialConfirmation,
submitText: values.submitText,
successText: values.successText,
})
@@ -528,13 +527,15 @@ export default function OrderServicePanel() {
<section className="order-service-section">
<div className="order-service-section-head">
<div>
<Text strong>页面公告</Text>
<Text type="secondary">显示在客户资料页顶部,可自定义内容和文字样式。</Text>
<Text strong>提交资料确认</Text>
<Text type="secondary">
客户点击提交时弹窗确认,可自定义提示内容和按钮文字。
</Text>
</div>
<Space size={8}>
<Text type="secondary">显示公告</Text>
<Text type="secondary">启用确认</Text>
<Form.Item
name={['announcement', 'enabled']}
name={['materialConfirmation', 'enabled']}
valuePropName="checked"
noStyle
>
@@ -543,63 +544,33 @@ export default function OrderServicePanel() {
</Space>
</div>
<Row gutter={[12, 0]}>
<Col xs={24} md={10}>
<Form.Item label="公告标题" name={['announcement', 'title']}>
<Input maxLength={30} placeholder="例如:公告" />
<Col xs={24} md={12}>
<Form.Item label="确认标题" name={['materialConfirmation', 'title']}>
<Input maxLength={40} placeholder="例如:提交资料确认" />
</Form.Item>
</Col>
<Col xs={12} md={4}>
<Form.Item label="字号" name={['announcement', 'fontSize']}>
<InputNumber
min={12}
max={24}
addonAfter="px"
style={{ width: '100%' }}
/>
<Col xs={12} md={6}>
<Form.Item label="确认按钮" name={['materialConfirmation', 'confirmText']}>
<Input maxLength={12} placeholder="确认提交" />
</Form.Item>
</Col>
<Col xs={12} md={5}>
<Form.Item label="文字颜色" name={['announcement', 'color']}>
<Input type="color" className="order-service-color-input" />
</Form.Item>
</Col>
<Col xs={12} md={5}>
<Form.Item label="背景颜色" name={['announcement', 'backgroundColor']}>
<Input type="color" className="order-service-color-input" />
</Form.Item>
</Col>
<Col xs={12} md={4}>
<Form.Item label="字重" name={['announcement', 'fontWeight']}>
<Select
options={[
{ value: 'normal', label: '正常' },
{ value: 'bold', label: '加粗' },
]}
/>
</Form.Item>
</Col>
<Col xs={12} md={4}>
<Form.Item label="对齐" name={['announcement', 'textAlign']}>
<Select
options={[
{ value: 'left', label: '左对齐' },
{ value: 'center', label: '居中' },
]}
/>
<Col xs={12} md={6}>
<Form.Item label="取消按钮" name={['materialConfirmation', 'cancelText']}>
<Input maxLength={12} placeholder="返回检查" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="公告内容" name={['announcement', 'content']}>
<Form.Item label="确认内容" name={['materialConfirmation', 'content']}>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 5 }}
autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={500}
showCount
placeholder="请输入需要客户注意的公告内容"
placeholder="请输入客户提交资料前需要确认的内容"
/>
</Form.Item>
</Col>
</Row>
<AnnouncementPreview announcement={announcementPreview} />
<MaterialConfirmationPreview confirmation={materialConfirmationPreview} />
</section>
<section className="order-service-section">
@@ -903,28 +874,30 @@ function resolveProfileStatus(profile: AdminOrderServiceProfile | undefined) {
return { label: '已归档', tone: 'muted' }
}
function AnnouncementPreview({ announcement }: { announcement: OrderServiceAnnouncement }) {
function MaterialConfirmationPreview({
confirmation,
}: {
confirmation: OrderServiceMaterialConfirmation
}) {
return (
<div
className={`order-service-announcement-preview${announcement.enabled ? '' : ' is-disabled'}`}
style={{
color: announcement.color,
backgroundColor: announcement.backgroundColor,
textAlign: announcement.textAlign,
}}
className={`order-service-confirmation-preview${confirmation.enabled ? '' : ' is-disabled'}`}
>
<Text strong className="order-service-announcement-preview-title">
{announcement.title || '公告'}
</Text>
<div
style={{
fontSize: announcement.fontSize,
fontWeight: announcement.fontWeight,
}}
>
{announcement.content || '公告内容'}
<strong>{confirmation.title || '提交资料确认'}</strong>
<div className="order-service-confirmation-preview-summary">
<span>商品:示例商品</span>
<span>游戏账号:customer@example.com</span>
<span>区服:安卓</span>
<span>截图:2 张</span>
</div>
{!announcement.enabled ? <span>当前已关闭,不会在客户页显示</span> : null}
<div className="order-service-confirmation-preview-content">
{confirmation.content || '提交资料确认内容'}
</div>
<div className="order-service-confirmation-preview-actions">
<span>{confirmation.cancelText || '返回检查'}</span>
<span className="is-primary">{confirmation.confirmText || '确认提交'}</span>
</div>
{!confirmation.enabled ? <small>当前已关闭,点击提交将直接提交资料</small> : null}
</div>
)
}
@@ -56,7 +56,7 @@ function resolveErrorTitle(code: string) {
}
export default function OrderServicePage() {
const { message } = App.useApp()
const { message, modal } = App.useApp()
const navigate = useNavigate()
const { orderNo: routeOrderNo } = useParams<{ orderNo?: string }>()
const orderNo = String(routeOrderNo || '').trim()
@@ -68,6 +68,7 @@ export default function OrderServicePage() {
const [submittingId, setSubmittingId] = useState(0)
const [screenshotsByOrder, setScreenshotsByOrder] = useState<Record<number, UploadedFile[]>>({})
const submittingRef = useRef(false)
const confirmationOpenRef = useRef(false)
const requestSeqRef = useRef(0)
const loadedOrderRef = useRef('')
@@ -146,9 +147,10 @@ export default function OrderServicePage() {
void navigate(`/o/${encodeURIComponent(nextOrderNo)}`)
}
async function handleSubmit(item: OrderServiceItem, values: Record<string, unknown>) {
if (!orderNo || submittingRef.current) return
function handleSubmit(item: OrderServiceItem, values: Record<string, unknown>) {
if (!orderNo || submittingRef.current || confirmationOpenRef.current) return
const screenshots = screenshotsByOrder[item.workOrderId] ?? item.screenshots ?? []
const normalizedValues = normalizeFormFieldValues(item, values)
const screenshotRule = item.screenshotsRule
if (
screenshotRule?.enabled &&
@@ -159,12 +161,51 @@ export default function OrderServicePage() {
return
}
const confirmation = item.materialConfirmation
if (!confirmation?.enabled) {
void submitMaterial(item, normalizedValues, screenshots)
return
}
confirmationOpenRef.current = true
modal.confirm({
title: confirmation.title || '提交资料确认',
content: (
<MaterialConfirmationSummary
item={item}
platformOrderId={orderNo}
values={normalizedValues}
screenshots={screenshots}
notice={
confirmation.content ||
'请确认资料填写准确无误。资料提交后不能修改,因个人资料填写错误导致的赠送错误,店铺概不负责!'
}
/>
),
okText: confirmation.confirmText || '确认提交',
cancelText: confirmation.cancelText || '返回检查',
centered: true,
maskClosable: false,
width: 560,
onOk: () => submitMaterial(item, normalizedValues, screenshots),
afterClose: () => {
confirmationOpenRef.current = false
},
})
}
async function submitMaterial(
item: OrderServiceItem,
fields: Record<string, string>,
screenshots: UploadedFile[],
) {
if (!orderNo || submittingRef.current) return
submittingRef.current = true
setSubmittingId(item.workOrderId)
try {
const response = await submitOrderServiceMaterial(orderNo, {
workOrderId: item.workOrderId,
fields: normalizeFormFieldValues(item, values),
fields,
screenshots,
})
setSubmittedNames((current) => [
@@ -261,31 +302,6 @@ export default function OrderServicePage() {
</Button>
</div>
{pageConfig?.announcement?.enabled && pageConfig.announcement.content ? (
<section
aria-label={pageConfig.announcement.title || '页面公告'}
className="collect-link-announcement"
style={{
color: pageConfig.announcement.color,
backgroundColor: pageConfig.announcement.backgroundColor,
textAlign: pageConfig.announcement.textAlign,
}}
>
<strong className="collect-link-announcement-title">
{pageConfig.announcement.title || '公告'}
</strong>
<div
className="collect-link-announcement-content"
style={{
fontSize: pageConfig.announcement.fontSize,
fontWeight: pageConfig.announcement.fontWeight,
}}
>
{pageConfig.announcement.content}
</div>
</section>
) : null}
{submittedNames.length > 0 ? (
<Alert
type="success"
@@ -509,6 +525,66 @@ function renderFieldInput(field: NonNullable<OrderServiceItem['fields']>[number]
return <Input autoComplete="off" placeholder={placeholder} />
}
function MaterialConfirmationSummary({
item,
platformOrderId,
values,
screenshots,
notice,
}: {
item: OrderServiceItem
platformOrderId: string
values: Record<string, string>
screenshots: UploadedFile[]
notice: string
}) {
const screenshotRule = item.screenshotsRule
return (
<div className="collect-link-confirm">
<div className="collect-link-confirm-meta">
<span>{item.productName || item.categoryName || '订单商品'}</span>
<span>订单号:{platformOrderId}</span>
</div>
{item.fields && item.fields.length > 0 ? (
<div className="collect-link-confirm-fields">
{item.fields.map((field) => {
const value = String(values[field.key] || '').trim()
return (
<div className="collect-link-confirm-field" key={field.key}>
<span>{field.label}</span>
<strong className={value ? '' : 'is-empty'}>{value || '未填写'}</strong>
</div>
)
})}
</div>
) : null}
{screenshotRule?.enabled ? (
<div className="collect-link-confirm-screenshots">
<div>
<span>{screenshotRule.label || '截图'}</span>
<strong>{screenshots.length} 张</strong>
</div>
{screenshots.length > 0 ? (
<div className="collect-link-confirm-images">
{screenshots.map((file) => (
<img
key={file.objectKey}
alt={file.filename || '已上传截图'}
src={file.thumbnailUrl || file.url}
/>
))}
</div>
) : null}
</div>
) : null}
<div className="collect-link-confirm-content">{notice}</div>
</div>
)
}
function normalizeFormFieldValues(item: OrderServiceItem, values: Record<string, unknown>) {
const fields = new Map((item.fields || []).map((field) => [field.key, field]))
return Object.fromEntries(
@@ -1120,15 +1120,12 @@ export type OrderServiceField = {
sortOrder?: number
}
export type OrderServiceAnnouncement = {
export type OrderServiceMaterialConfirmation = {
enabled: boolean
title: string
content: string
fontSize: number
color: string
backgroundColor: string
fontWeight: 'normal' | 'bold'
textAlign: 'left' | 'center'
confirmText: string
cancelText: string
}
export type AdminOrderServiceProfile = {
@@ -1152,7 +1149,7 @@ export type AdminOrderServiceProfile = {
requireVoucherConsume: boolean
retryMax: number
}
announcement: OrderServiceAnnouncement
materialConfirmation: OrderServiceMaterialConfirmation
submitText: string
successText: string
status: 'draft' | 'published' | 'archived' | string
@@ -1199,7 +1196,7 @@ export function saveAdminOrderServiceProfile(payload: {
requireVoucherConsume: boolean
retryMax: number
}
announcement: OrderServiceAnnouncement
materialConfirmation: OrderServiceMaterialConfirmation
submitText?: string
successText?: string
}) {
+4 -7
View File
@@ -576,15 +576,12 @@ export type OrderServiceScreenshots = {
helpText?: string
}
export type OrderServiceAnnouncement = {
export type OrderServiceMaterialConfirmation = {
enabled: boolean
title: string
content: string
fontSize: number
color: string
backgroundColor: string
fontWeight: 'normal' | 'bold'
textAlign: 'left' | 'center'
confirmText: string
cancelText: string
}
export type OrderServiceItem = {
@@ -600,7 +597,7 @@ export type OrderServiceItem = {
message: string
canSubmit: boolean
blockedReason: '' | 'already_submitted' | 'status' | 'assigned' | 'shares'
announcement?: OrderServiceAnnouncement
materialConfirmation?: OrderServiceMaterialConfirmation
fields?: OrderServiceField[]
values?: Record<string, string>
screenshots?: UploadedFile[]
+49 -16
View File
@@ -1843,31 +1843,64 @@
margin-top: 6px;
}
.order-service-color-input {
height: 32px;
padding: 2px 3px;
cursor: pointer;
}
.order-service-announcement-preview {
.order-service-confirmation-preview {
display: grid;
gap: 4px;
padding: 12px 14px;
border: 1px solid rgba(138, 75, 0, 0.16);
border-left-width: 4px;
border-radius: 9px;
gap: 10px;
max-width: 560px;
padding: 16px;
color: #1f2937;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
}
.order-service-announcement-preview-title {
color: inherit !important;
.order-service-confirmation-preview-content {
color: #64748b;
font-size: 13px;
line-height: 1.65;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.order-service-announcement-preview.is-disabled {
.order-service-confirmation-preview-summary {
display: grid;
gap: 6px;
padding: 10px 12px;
color: #334155;
font-size: 12px;
background: #f8fafc;
border: 1px solid #edf2f7;
border-radius: 8px;
}
.order-service-confirmation-preview-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 6px;
border-top: 1px solid #f1f5f9;
}
.order-service-confirmation-preview-actions span {
padding: 3px 11px;
color: #475569;
font-size: 12px;
border: 1px solid #dbe2ea;
border-radius: 6px;
}
.order-service-confirmation-preview-actions .is-primary {
color: #ffffff;
background: #1677ff;
border-color: #1677ff;
}
.order-service-confirmation-preview.is-disabled {
opacity: 0.55;
}
.order-service-announcement-preview > span:last-child {
.order-service-confirmation-preview > small {
font-size: 11px;
}
+97 -9
View File
@@ -515,21 +515,104 @@
gap: 12px;
}
.collect-link-announcement {
padding: 12px 14px;
border: 1px solid rgba(138, 75, 0, 0.16);
border-left-width: 4px;
.collect-link-confirm {
display: grid;
gap: 12px;
max-height: 58vh;
padding-top: 4px;
padding-right: 4px;
overflow-y: auto;
}
.collect-link-confirm-meta {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 4px 16px;
color: #64748b;
font-size: 12px;
}
.collect-link-confirm-meta span:first-child {
color: #0f172a;
font-size: 14px;
font-weight: 600;
}
.collect-link-confirm-fields {
overflow: hidden;
border: 1px solid #e8edf4;
border-radius: 9px;
}
.collect-link-announcement-title {
display: block;
margin-bottom: 4px;
.collect-link-confirm-field {
display: grid;
grid-template-columns: 110px minmax(0, 1fr);
gap: 12px;
padding: 9px 12px;
font-size: 13px;
border-bottom: 1px solid #eef2f7;
}
.collect-link-announcement-content {
line-height: 1.65;
.collect-link-confirm-field:last-child {
border-bottom: 0;
}
.collect-link-confirm-field span,
.collect-link-confirm-screenshots span {
color: #64748b;
}
.collect-link-confirm-field strong,
.collect-link-confirm-screenshots strong {
color: #0f172a;
font-weight: 500;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.collect-link-confirm-field strong.is-empty {
color: #94a3b8;
}
.collect-link-confirm-screenshots {
display: grid;
gap: 8px;
padding: 10px 12px;
font-size: 13px;
background: #f8fafc;
border-radius: 9px;
}
.collect-link-confirm-screenshots > div:first-child {
display: flex;
justify-content: space-between;
gap: 12px;
}
.collect-link-confirm-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.collect-link-confirm-images img {
width: 58px;
height: 58px;
object-fit: cover;
background: #ffffff;
border: 1px solid #dbe3ee;
border-radius: 8px;
}
.collect-link-confirm-content {
padding: 10px 12px;
color: #8a4b00;
font-size: 13px;
line-height: 1.7;
background: #fff7e6;
border: 1px solid #ffe7ba;
border-radius: 9px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
@@ -687,6 +770,11 @@
.collect-link-success-products {
padding: 10px 16px;
}
.collect-link-confirm-field {
grid-template-columns: 88px minmax(0, 1fr);
padding: 8px 10px;
}
}
/* ===== 兑换中心 (Redeem Page) ===== */
@@ -92,7 +92,7 @@ work_orders.service_profile_snapshot_json
- 按平台订单号查询一单多工单。
- 动态文本、长文本、下拉、单选、数字、日期字段。
- 一张分类引导图。
- 页面公告,可配置标题、正文、字号、文字颜色、背景颜色、字重和对齐方式;旧配置默认显示资料填写错误免责声明。
- 提交资料确认弹窗,先逐项汇总用户填写的字段值和截图缩略图,再展示可配置的标题、正文、确认/取消按钮文字;旧配置默认提示资料提交后不可修改,并说明资料填写错误的免责规则。
- 图片截图规则,必填时最少 1 张。
- 提交后禁止再次修改。
- 每 15 秒、页面重新可见或窗口重新聚焦时刷新状态。