From 3a557e8bdab8e062aa0450f4e08239b87c715f1c Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 4 Nov 2026 21:26:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(order-service):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E5=BC=95=E5=AF=BC=E5=9B=BE=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=8E=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../order-service-profile-service.test.ts | 13 +++ .../order-service-profile-service.ts | 37 ++++++- .../order-service-public-service.ts | 3 + .../pages/admin/panels/OrderServicePanel.tsx | 60 ++++++++--- .../src/pages/public/OrderServicePage.tsx | 69 +++++++----- .../src/services/admin/worker-platform.ts | 2 + apps/frontend/src/services/worker.ts | 1 + apps/frontend/src/styles/admin.css | 69 +++++++----- apps/frontend/src/styles/client.css | 100 ++++++++++++++++++ 9 files changed, 283 insertions(+), 71 deletions(-) diff --git a/apps/backend/src/services/worker-platform/order-service-profile-service.test.ts b/apps/backend/src/services/worker-platform/order-service-profile-service.test.ts index 6c72f688..df0845e1 100644 --- a/apps/backend/src/services/worker-platform/order-service-profile-service.test.ts +++ b/apps/backend/src/services/worker-platform/order-service-profile-service.test.ts @@ -94,6 +94,19 @@ test('订单服务提交后说明:旧配置使用默认文案,新配置支 ) }) +test('用户引导图:说明文字随图片配置保存,旧配置默认不显示说明', () => { + assert.equal(config().guideImageDescription, '') + const normalized = config({ + guideImage: { + objectKey: 'order-service/guide.webp', + url: '/api/v1/files/object?key=order-service%2Fguide.webp', + }, + guideImageDescription: '请截图买家个人主页,并确认游戏 ID 清晰可见。', + }) + assert.equal(normalized.guideImageDescription, '请截图买家个人主页,并确认游戏 ID 清晰可见。') + assert.equal(normalized.guideImage.description, '请截图买家个人主页,并确认游戏 ID 清晰可见。') +}) + test('客户状态进度:四个节点支持独立标题和说明,旧配置使用默认文案', () => { assert.deepEqual(config().statusSteps, { submitted: { title: '提交资料', description: '' }, diff --git a/apps/backend/src/services/worker-platform/order-service-profile-service.ts b/apps/backend/src/services/worker-platform/order-service-profile-service.ts index 6513dfb9..d2c4d94b 100644 --- a/apps/backend/src/services/worker-platform/order-service-profile-service.ts +++ b/apps/backend/src/services/worker-platform/order-service-profile-service.ts @@ -69,6 +69,7 @@ export type OrderServiceProfileConfig = { title: string description: string guideImage: Record + guideImageDescription: string fields: OrderServiceField[] screenshots: OrderServiceScreenshots autoPublish: OrderServiceAutoPublish @@ -157,7 +158,14 @@ export async function saveAdminOrderServiceProfile(payload: JsonObject = {}) { payload.materialConfirmation ?? payload.material_confirmation_json, ) const statusSteps = normalizeStatusSteps(payload.statusSteps ?? payload.status_steps_json) - const guideImage = normalizeGuideImage(payload.guideImage ?? payload.guide_image_json) + const guideImageDescription = text( + payload.guideImageDescription ?? payload.guide_image_description, + 200, + ) + const guideImage = normalizeGuideImage( + payload.guideImage ?? payload.guide_image_json, + guideImageDescription, + ) const adminUserId = Number(payload.adminUserId ?? payload.admin_user_id ?? 0) || null const saved = await upsertOrderServiceProfile({ @@ -246,6 +254,7 @@ function mapAdminProfile(row: OrderServiceProfileRow) { title: row.title, description: row.description, guideImage: row.guide_image_json, + guideImageDescription: buildProfileConfig(row).guideImageDescription, fields: buildProfileConfig(row).fields, screenshots: row.screenshots_json, autoPublish: row.auto_publish_json, @@ -276,11 +285,13 @@ export function buildProfileConfig(row: { success_text: string post_submit_text: string }): OrderServiceProfileConfig & { guideImage: Record } { + const guideImage = normalizeGuideImage(row.guide_image_json) return { name: row.name, title: row.title, description: row.description, - guideImage: normalizeGuideImage(row.guide_image_json), + guideImage, + guideImageDescription: readGuideImageDescription(guideImage), fields: normalizeFields(row.fields_json), screenshots: normalizeScreenshots(row.screenshots_json), autoPublish: normalizeAutoPublish(row.auto_publish_json), @@ -330,11 +341,16 @@ export function isOrderServiceMaterialRequired(config: OrderServiceProfileConfig } export function normalizeProfileConfig(value: JsonObject): OrderServiceProfileConfig { + const guideImage = normalizeGuideImage( + value.guideImage ?? value.guide_image, + value.guideImageDescription ?? value.guide_image_description, + ) return { name: text(value.name, 80) || '订单服务配置', title: text(value.title, 80), description: text(value.description, 300), - guideImage: normalizeGuideImage(value.guideImage ?? value.guide_image), + guideImage, + guideImageDescription: readGuideImageDescription(guideImage), fields: normalizeFields(value.fields), screenshots: normalizeScreenshots(value.screenshots), autoPublish: normalizeAutoPublish(value.autoPublish), @@ -459,9 +475,20 @@ function normalizeStatusStep(value: unknown, fallback: { title: string; descript } } -function normalizeGuideImage(value: unknown): Record { +function normalizeGuideImage(value: unknown, descriptionInput?: unknown): Record { + const source = + value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} const normalized = normalizeUploadedFiles([value])[0] - return normalized ? { ...normalized } : {} + if (!normalized) return {} + const description = text(descriptionInput ?? source.description ?? source.guideDescription, 200) + return { + ...normalized, + ...(description ? { description } : {}), + } +} + +function readGuideImageDescription(value: Record) { + return text(value.description, 200) } function clampInteger(value: unknown, fallback: number, min: number, max: number) { diff --git a/apps/backend/src/services/worker-platform/order-service-public-service.ts b/apps/backend/src/services/worker-platform/order-service-public-service.ts index 6299ab70..e18f2e2f 100644 --- a/apps/backend/src/services/worker-platform/order-service-public-service.ts +++ b/apps/backend/src/services/worker-platform/order-service-public-service.ts @@ -93,6 +93,7 @@ export type OrderServicePublicItem = { screenshots?: Array> screenshotsRule?: OrderServiceProfileConfig['screenshots'] guideImage?: Record + guideImageDescription?: string acceptanceImageUrls?: string[] acceptanceVideo?: OrderServiceAcceptanceVideo submittedAt?: string @@ -537,6 +538,7 @@ async function mapPublicOrderItem(order: WorkOrderRow): Promise + const url = String(source.url || '').trim() + const objectKey = String(source.objectKey || source.object_key || '').trim() + return url || objectKey ? (value as UploadedFile) : null +} + export default function OrderServicePanel() { const { message } = App.useApp() const queryClient = useQueryClient() @@ -275,7 +284,10 @@ export default function OrderServicePanel() { ).length const selectedCategory = categories.find((item) => item.categoryId === selectedCategoryId) || null const profile = (selectedCategoryId ? profileByCategoryId.get(selectedCategoryId) : null) || null - const guideFiles = useMemo(() => (guideImage ? [guideImage as UploadedFile] : []), [guideImage]) + const guideFiles = useMemo(() => { + const image = resolveGuideImage(guideImage) + return image ? [image] : [] + }, [guideImage]) const handleGuideImageChange = useCallback((files: UploadedFile[]) => { setGuideImage(files[0] || null) }, []) @@ -302,6 +314,7 @@ export default function OrderServicePanel() { name: nextProfile?.name || selectedCategory?.name || '', title: nextProfile?.title || '', description: nextProfile?.description || '', + guideImageDescription: nextProfile?.guideImageDescription || '', submitText: nextProfile?.submitText || '提交资料', successText: nextProfile?.successText || '资料已提交,请等待处理', postSubmitText: nextProfile?.postSubmitText || DEFAULT_POST_SUBMIT_TEXT, @@ -315,7 +328,7 @@ export default function OrderServicePanel() { nextProfile ? nextProfile.fields : buildPresetFields(GAME_BASIC_PRESET_KEYS), ), ) - setGuideImage(nextProfile?.guideImage || null) + setGuideImage(resolveGuideImage(nextProfile?.guideImage)) } async function refresh() { @@ -413,6 +426,7 @@ export default function OrderServicePanel() { title: values.title || '', description: values.description || '', guideImage: guideImage || {}, + guideImageDescription: values.guideImageDescription || '', fields: normalizedFields, screenshots: values.screenshots, autoPublish: values.autoPublish, @@ -652,7 +666,7 @@ export default function OrderServicePanel() { />
-
+
@@ -699,16 +713,36 @@ export default function OrderServicePanel() {
- 用户引导图 - 首版一张,展示在字段上方 - +
+ 用户引导图 + + 一张,显示在客户页“买家主页截图”上传区右侧,可点击放大查看 + +
+
+ +
+ + +
diff --git a/apps/frontend/src/pages/public/OrderServicePage.tsx b/apps/frontend/src/pages/public/OrderServicePage.tsx index e98590a2..b104a955 100644 --- a/apps/frontend/src/pages/public/OrderServicePage.tsx +++ b/apps/frontend/src/pages/public/OrderServicePage.tsx @@ -601,6 +601,8 @@ function OrderServiceForm({ label: '截图', helpText: '', } + const guideImageUrl = + item.guideImage && 'url' in item.guideImage ? String(item.guideImage.url || '').trim() : '' return ( @@ -618,14 +620,6 @@ function OrderServiceForm({ {item.title ? {item.title} : null} {item.description ? {item.description} : null} - {item.guideImage && 'url' in item.guideImage && item.guideImage.url ? ( - 填写引导图 - ) : null} - {item.fields && item.fields.length > 0 ? (
{item.fields.map((field) => ( @@ -672,24 +666,47 @@ function OrderServiceForm({ } extra={rule.helpText} > - { - const response = await uploadOrderServiceFile( - file, - scene, - code, - item.workOrderId, - ) - return response.data.file - }} - /> +
+
+ { + const response = await uploadOrderServiceFile( + file, + scene, + code, + item.workOrderId, + ) + return response.data.file + }} + /> +
+ {guideImageUrl ? ( + + ) : null} +
) : null} diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index 3ec8dcc0..c3c3acb1 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -1143,6 +1143,7 @@ export type AdminOrderServiceProfile = { title: string description: string guideImage: UploadedFile | Record + guideImageDescription: string fields: OrderServiceField[] screenshots: { enabled: boolean @@ -1192,6 +1193,7 @@ export function saveAdminOrderServiceProfile(payload: { title?: string description?: string guideImage?: UploadedFile | Record + guideImageDescription?: string fields: OrderServiceField[] screenshots: { enabled: boolean diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index 3eb02b6c..47093005 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -613,6 +613,7 @@ export type OrderServiceItem = { screenshots?: UploadedFile[] screenshotsRule?: OrderServiceScreenshots guideImage?: UploadedFile | Record + guideImageDescription?: string acceptanceImageUrls?: string[] acceptanceVideo?: { url: string diff --git a/apps/frontend/src/styles/admin.css b/apps/frontend/src/styles/admin.css index aae9ff1d..502fcc98 100644 --- a/apps/frontend/src/styles/admin.css +++ b/apps/frontend/src/styles/admin.css @@ -1858,26 +1858,59 @@ .order-service-basic-grid { display: grid; - grid-template-columns: minmax(0, 1fr) 230px; - gap: 18px; + gap: 14px; } .order-service-basic-grid .ant-form-item { margin-bottom: 12px; } +.order-service-basic-fields { + min-width: 0; +} + .order-service-guide { display: grid; + grid-template-columns: minmax(180px, 220px) minmax(260px, 1fr); + grid-template-rows: auto 1fr; + align-items: start; align-content: start; - gap: 4px; - padding: 12px; + gap: 12px 18px; + padding: 14px; background: #f8fafc; border: 1px dashed #d9e1ec; border-radius: 10px; } -.order-service-guide .ant-upload-wrapper { - margin-top: 6px; +.order-service-guide-head { + grid-column: 1 / -1; + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 4px 10px; +} + +.order-service-guide-head .ant-typography:last-child { + font-size: 12px; +} + +.order-service-guide-upload { + min-width: 0; +} + +.order-service-guide-upload .ant-upload-list-item-container, +.order-service-guide-upload .ant-upload-select { + width: 180px !important; + height: 140px !important; +} + +.order-service-guide-description.ant-form-item { + margin-bottom: 0; +} + +.order-service-guide-description .ant-form-item-extra { + margin-top: 2px; + font-size: 11px; } .order-service-status-step-grid { @@ -2170,10 +2203,6 @@ grid-template-columns: minmax(120px, 1fr) minmax(140px, 1fr) 110px 64px 104px; } - .order-service-basic-grid { - grid-template-columns: minmax(0, 1fr) 200px; - } - .order-service-status-step-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -2206,21 +2235,8 @@ padding: 16px 16px 0; } - .order-service-basic-grid { - grid-template-columns: 1fr; - } - .order-service-guide { - grid-template-columns: minmax(0, 1fr) 160px; - } - - .order-service-guide > :not(.ant-upload-wrapper) { - grid-column: 1; - } - - .order-service-guide .ant-upload-wrapper { - grid-column: 2; - grid-row: 1 / span 3; + grid-template-columns: minmax(160px, 200px) minmax(0, 1fr); } .order-service-section-head { @@ -2270,12 +2286,11 @@ .order-service-guide { grid-template-columns: 1fr; + grid-template-rows: auto; } - .order-service-guide > :not(.ant-upload-wrapper), - .order-service-guide .ant-upload-wrapper { + .order-service-guide-head { grid-column: 1; - grid-row: auto; } .order-service-status-step-grid { diff --git a/apps/frontend/src/styles/client.css b/apps/frontend/src/styles/client.css index 537d83e0..42e7c49f 100644 --- a/apps/frontend/src/styles/client.css +++ b/apps/frontend/src/styles/client.css @@ -1060,6 +1060,64 @@ margin-top: 18px; } +.collect-link-screenshot-control { + display: grid; + gap: 14px; + align-items: start; +} + +.collect-link-screenshot-control.has-guide { + grid-template-columns: 120px minmax(180px, 260px); + gap: 16px; +} + +.collect-link-guide-example { + display: grid; + grid-template-columns: 1fr; + gap: 6px; + min-width: 0; + max-width: 100%; + align-content: start; + justify-items: start; +} + +.collect-link-guide-example .ant-image { + display: block; + width: 128px !important; + height: 88px !important; + overflow: hidden; + border: 1px solid #dbe3ee; + border-radius: 7px; + cursor: zoom-in; +} + +.collect-link-guide-example .ant-image-img { + display: block; + width: 128px !important; + height: 88px !important; + object-fit: cover; + object-position: center; +} + +.collect-link-guide-copy { + display: grid; + align-content: center; + gap: 3px; + min-width: 0; +} + +.collect-link-guide-copy .ant-typography:first-child { + color: #334155; + font-size: 12px; +} + +.collect-link-guide-copy .ant-typography:last-child { + max-width: 220px; + font-size: 11px; + line-height: 1.55; + white-space: pre-wrap; +} + .collect-link-actions { display: flex; justify-content: flex-end; @@ -1086,6 +1144,21 @@ padding: 18px; } + .collect-link-screenshot-control.has-guide { + grid-template-columns: 112px minmax(160px, 220px); + gap: 12px; + } + + .collect-link-guide-example .ant-image { + width: 116px !important; + height: 80px !important; + } + + .collect-link-guide-example .ant-image-img { + width: 116px !important; + height: 80px !important; + } + .collect-link-actions { justify-content: stretch; } @@ -1104,6 +1177,33 @@ padding: 14px; } + .collect-link-screenshot-control.has-guide { + grid-template-columns: 104px minmax(0, 1fr); + gap: 8px; + } + + .collect-link-guide-example { + gap: 5px; + } + + .collect-link-guide-example .ant-image { + width: 96px !important; + height: 68px !important; + } + + .collect-link-guide-example .ant-image-img { + width: 96px !important; + height: 68px !important; + } + + .collect-link-guide-copy .ant-typography:first-child { + font-size: 11px; + } + + .collect-link-guide-copy .ant-typography:last-child { + font-size: 10px; + } + .collect-link-success { min-height: 320px; padding: 16px 4px 4px;