From 6d9344033464c2543cab9b158c953b208cfa7667 Mon Sep 17 00:00:00 2001 From: czz <862977248@qq.com> Date: Fri, 8 May 2026 14:59:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E6=8A=A5=E4=BF=AE=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 35 ++ package.json | 3 +- scripts/tests/repair-utils.test.cjs | 85 +++++ src/app.config.ts | 1 + src/pages/repair-detail/index.config.ts | 6 + src/pages/repair-detail/index.scss | 156 +++++++++ src/pages/repair-detail/index.tsx | 99 ++++++ src/pages/repair/index.config.ts | 5 +- src/pages/repair/index.scss | 416 ++++++++++++++++++++++++ src/pages/repair/index.tsx | 399 +++++++++++++++++++++-- src/pages/repair/repair-utils.ts | 144 ++++++++ tsconfig.report-tests.json | 2 +- 12 files changed, 1328 insertions(+), 23 deletions(-) create mode 100644 scripts/tests/repair-utils.test.cjs create mode 100644 src/pages/repair-detail/index.config.ts create mode 100644 src/pages/repair-detail/index.scss create mode 100644 src/pages/repair-detail/index.tsx create mode 100644 src/pages/repair/repair-utils.ts diff --git a/README.md b/README.md index 0dfc457..9181454 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ - 通用 UI 优先拆成可复用组件,方便后续多页面共用 当前项目还新增了一版“睡眠报告演示页”,用于展示宝宝睡眠评分、呼吸状态、HRV、异常统计和 AI 分析建议等完整报告结构。 +同时也补上了一版“申请报修演示页”,用于展示设备报修表单、附件上传与提交成功后的详情页流程。 ## 1. 目前已经包含什么 - `Taro + React + TypeScript` 项目骨架 - 首页设备绑定业务示例页面 - 睡眠报告演示页面 +- 申请报修演示页面 - 小程序 `AppID` 配置 - `AGENTS.md` 协作规则文件 - 从开发到发布的中文说明 @@ -238,6 +240,26 @@ npm run dev:weapp - 各板块尽量拆成了可复用组件,便于你继续改 - 分享功能当前还是前端占位提示,后续可再接真实能力 +## 12.2 当前报修页做了什么 + +现在项目里已经把原来的“设备报修”占位页补成了一版可交互的“申请报修页”,主要包含: + +- 使用小程序原生导航栏显示“申请报修” +- `体征监测设备 / AI摄像头` 两种设备类型切换 +- 已绑定设备 ID 选择与设备参数自动带出 +- 60 字以内的问题描述输入与实时字数统计 +- 调用小程序 `chooseMedia` 选择图片或视频附件 +- 图片最多 9 张、视频最多 1 个的前端限制 +- 联系人和手机号填写 +- 提交前必填校验与手机号格式校验 +- mock 提交成功后跳转到“报修详情”占位页 + +说明: + +- 当前报修页使用本地 mock 数据驱动 +- 附件当前只做本地选择、预览和删除,不上传到真实服务器 +- 历史记录按钮当前还是前端提示占位,后续可再接真实列表页 + ## 13. 你接下来最常做的开发动作 ### 改公共主题色 @@ -276,6 +298,13 @@ npm run dev:weapp - `src/components/report/` 目录下的组件文件 +### 改报修页 + +编辑: + +- [src/pages/repair/index.tsx](C:/Users/a/Documents/New%20project%203/src/pages/repair/index.tsx) +- [src/pages/repair-detail/index.tsx](C:/Users/a/Documents/New%20project%203/src/pages/repair-detail/index.tsx) + ### 改全局样式 编辑: @@ -388,6 +417,12 @@ npm run build:weapp npm run test:report ``` +如果你要验证报修页里的表单和附件规则纯逻辑,可以执行: + +```bash +npm run test:repair +``` + ## 17. 建议你下一步怎么做 如果你是零基础,推荐按这个顺序继续: diff --git a/package.json b/package.json index e2509ee..1565931 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev:weapp": "taro build --type weapp --watch", "build:weapp": "taro build --type weapp", - "test:report": "tsc -p tsconfig.report-tests.json && node scripts/tests/report-utils.test.cjs" + "test:report": "tsc -p tsconfig.report-tests.json && node scripts/tests/report-utils.test.cjs", + "test:repair": "tsc -p tsconfig.report-tests.json && node scripts/tests/repair-utils.test.cjs" }, "dependencies": { "@tarojs/components": "^4.0.0", diff --git a/scripts/tests/repair-utils.test.cjs b/scripts/tests/repair-utils.test.cjs new file mode 100644 index 0000000..34c1f5d --- /dev/null +++ b/scripts/tests/repair-utils.test.cjs @@ -0,0 +1,85 @@ +const assert = require("node:assert/strict"); +const { + appendRepairAttachments, + clampDescription, + isValidPhone, + buildDeviceMap, + normalizeChosenFiles, + validateRepairForm +} = require("../../tmp/report-tests/repair/repair-utils.js"); + +function run(name, fn) { + try { + fn(); + console.log(`PASS ${name}`); + } catch (error) { + console.error(`FAIL ${name}`); + throw error; + } +} + +run("clampDescription trims to 60 chars", () => { + assert.equal(clampDescription("a".repeat(61)).length, 60); +}); + +run("isValidPhone accepts mainland mobile number", () => { + assert.equal(isValidPhone("13689569989"), true); +}); + +run("validateRepairForm rejects empty problem description", () => { + assert.equal( + validateRepairForm({ + selectedDeviceId: "A1", + description: "", + contactName: "张小龙", + phone: "13689569989" + }), + "请填写问题描述" + ); +}); + +run("buildDeviceMap groups devices by type", () => { + const map = buildDeviceMap([ + { id: "A1", type: "monitor", label: "设备A", params: "P1" }, + { id: "B1", type: "camera", label: "设备B", params: "P2" } + ]); + + assert.equal(map.monitor.length, 1); + assert.equal(map.camera[0].id, "B1"); +}); + +run("normalizeChosenFiles tags images and videos", () => { + const files = normalizeChosenFiles([ + { tempFilePath: "x/a.png", fileType: "image", size: 10 }, + { tempFilePath: "x/b.mp4", fileType: "video", size: 20, thumbTempFilePath: "x/b.jpg", duration: 5 } + ]); + + assert.equal(files[0].kind, "image"); + assert.equal(files[1].kind, "video"); + assert.equal(files[1].thumbPath, "x/b.jpg"); +}); + +run("appendRepairAttachments blocks second video", () => { + const result = appendRepairAttachments( + [{ id: "v1", kind: "video", path: "x/1.mp4" }], + [{ id: "v2", kind: "video", path: "x/2.mp4" }] + ); + + assert.equal(result.errorMessage, "最多上传1个视频"); + assert.equal(result.attachments.length, 1); +}); + +run("appendRepairAttachments limits images to 9", () => { + const existing = Array.from({ length: 8 }, (_, index) => ({ + id: `img-${index}`, + kind: "image", + path: `x/${index}.png` + })); + const result = appendRepairAttachments(existing, [ + { id: "img-8", kind: "image", path: "x/8.png" }, + { id: "img-9", kind: "image", path: "x/9.png" } + ]); + + assert.equal(result.errorMessage, "最多上传9张图片"); + assert.equal(result.attachments.length, 9); +}); diff --git a/src/app.config.ts b/src/app.config.ts index 83b242e..f14567d 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -9,6 +9,7 @@ export default defineAppConfig({ "pages/support/index", "pages/devices/index", "pages/repair/index", + "pages/repair-detail/index", "pages/feedback/index", "pages/videos/index", "pages/follow-us/index" diff --git a/src/pages/repair-detail/index.config.ts b/src/pages/repair-detail/index.config.ts new file mode 100644 index 0000000..33fe2ad --- /dev/null +++ b/src/pages/repair-detail/index.config.ts @@ -0,0 +1,6 @@ +export default definePageConfig({ + navigationBarTitleText: "报修详情", + navigationBarBackgroundColor: "#0B1220", + navigationBarTextStyle: "white", + backgroundColor: "#0B1220" +}); diff --git a/src/pages/repair-detail/index.scss b/src/pages/repair-detail/index.scss new file mode 100644 index 0000000..a35f4df --- /dev/null +++ b/src/pages/repair-detail/index.scss @@ -0,0 +1,156 @@ +.repair-detail-page { + position: relative; + min-height: 100vh; + padding: 28rpx 24rpx 72rpx; + box-sizing: border-box; + background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%); + overflow: hidden; +} + +.repair-detail-page__glow { + position: absolute; + border-radius: 50%; + pointer-events: none; +} + +.repair-detail-page__glow--left { + top: -60rpx; + left: -100rpx; + width: 300rpx; + height: 300rpx; + background: radial-gradient(circle, rgba(53, 229, 179, 0.16) 0%, rgba(53, 229, 179, 0) 72%); +} + +.repair-detail-page__glow--right { + top: 340rpx; + right: -120rpx; + width: 340rpx; + height: 340rpx; + background: radial-gradient(circle, rgba(59, 130, 246, 0.14) 0%, rgba(59, 130, 246, 0) 72%); +} + +.repair-detail-page__hero, +.repair-detail-page__card, +.repair-detail-page__button { + position: relative; + z-index: 1; +} + +.repair-detail-page__hero { + display: flex; + flex-direction: column; + align-items: center; + padding: 18rpx 0 38rpx; +} + +.repair-detail-page__success-ring { + position: relative; + width: 112rpx; + height: 112rpx; + border-radius: 50%; + background: linear-gradient(135deg, rgba(53, 229, 179, 0.3), rgba(32, 201, 191, 0.12)); + box-shadow: inset 0 0 0 2rpx rgba(53, 229, 179, 0.28); +} + +.repair-detail-page__success-check { + position: absolute; + top: 32rpx; + left: 42rpx; + width: 22rpx; + height: 40rpx; + border-right: 6rpx solid #35e5b3; + border-bottom: 6rpx solid #35e5b3; + transform: rotate(40deg); +} + +.repair-detail-page__status { + margin-top: 22rpx; + color: #ffffff; + font-size: 40rpx; + font-weight: 600; +} + +.repair-detail-page__hint { + width: 560rpx; + margin-top: 16rpx; + color: rgba(255, 255, 255, 0.58); + font-size: 24rpx; + text-align: center; + line-height: 1.7; +} + +.repair-detail-page__card { + margin-top: 22rpx; + padding: 28rpx; + border-radius: 28rpx; + background: rgba(34, 40, 57, 0.94); + box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.03); +} + +.repair-detail-page__row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20rpx; +} + +.repair-detail-page__row + .repair-detail-page__row { + margin-top: 20rpx; +} + +.repair-detail-page__label, +.repair-detail-page__meta-label { + color: rgba(255, 255, 255, 0.44); + font-size: 24rpx; +} + +.repair-detail-page__value, +.repair-detail-page__meta-value { + color: rgba(255, 255, 255, 0.9); + font-size: 24rpx; + text-align: right; + word-break: break-all; +} + +.repair-detail-page__value--accent { + color: #35e5b3; +} + +.repair-detail-page__section-title { + display: block; + color: rgba(255, 255, 255, 0.92); + font-size: 26rpx; +} + +.repair-detail-page__description { + display: block; + margin-top: 18rpx; + color: rgba(255, 255, 255, 0.72); + font-size: 24rpx; + line-height: 1.8; +} + +.repair-detail-page__meta-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 24rpx; + padding-top: 24rpx; + border-top: 2rpx solid rgba(255, 255, 255, 0.05); +} + +.repair-detail-page__button { + height: 96rpx; + margin-top: 44rpx; + border: none; + border-radius: 999rpx; + background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%); + color: #ffffff; + font-size: 32rpx; + font-weight: 600; + line-height: 96rpx; +} + +.repair-detail-page__button::after { + border: none; +} diff --git a/src/pages/repair-detail/index.tsx b/src/pages/repair-detail/index.tsx new file mode 100644 index 0000000..edd04a8 --- /dev/null +++ b/src/pages/repair-detail/index.tsx @@ -0,0 +1,99 @@ +import { Button, Text, View } from "@tarojs/components"; +import Taro from "@tarojs/taro"; +import { + REPAIR_DEVICE_TYPE_LABELS, + REPAIR_DRAFT_STORAGE_KEY, + type RepairSubmissionSnapshot +} from "../repair/repair-utils"; +import "./index.scss"; + +function getAttachmentSummary(detail?: RepairSubmissionSnapshot) { + if (!detail) { + return "0 个附件"; + } + + const imageCount = detail.attachments.filter((item) => item.kind === "image").length; + const videoCount = detail.attachments.filter((item) => item.kind === "video").length; + + if (!imageCount && !videoCount) { + return "0 个附件"; + } + + return `${imageCount} 张图片 / ${videoCount} 个视频`; +} + +export default function RepairDetailPage() { + const detail = Taro.getStorageSync(REPAIR_DRAFT_STORAGE_KEY) as RepairSubmissionSnapshot | undefined; + + return ( + + + + + + + + + 提交成功 + 报修申请已提交,售后人员会尽快审核并与你联系。 + + + + + 报修单号 + {detail?.ticketNo || "--"} + + + 提交时间 + {detail?.createdAt || "--"} + + + 当前状态 + {detail?.statusLabel || "已提交,待审核"} + + + + + + 设备类型 + + {detail ? REPAIR_DEVICE_TYPE_LABELS[detail.deviceType] : "--"} + + + + 设备ID + {detail?.deviceId || "--"} + + + 设备参数 + {detail?.deviceParams || "--"} + + + + + 问题描述 + {detail?.description || "暂无描述"} + + + 附件信息 + {getAttachmentSummary(detail)} + + + + + + 联系人 + {detail?.contactName || "--"} + + + 手机号 + {detail?.phone || "--"} + + + + + + ); +} diff --git a/src/pages/repair/index.config.ts b/src/pages/repair/index.config.ts index 26a3152..dc4cc0b 100644 --- a/src/pages/repair/index.config.ts +++ b/src/pages/repair/index.config.ts @@ -1,3 +1,6 @@ export default definePageConfig({ - navigationBarTitleText: "设备报修" + navigationBarTitleText: "申请报修", + navigationBarBackgroundColor: "#0B1220", + navigationBarTextStyle: "white", + backgroundColor: "#0B1220" }); diff --git a/src/pages/repair/index.scss b/src/pages/repair/index.scss index 0cef517..7a140ea 100644 --- a/src/pages/repair/index.scss +++ b/src/pages/repair/index.scss @@ -1,3 +1,419 @@ .repair-page { + position: relative; + min-height: 100vh; + padding: 28rpx 24rpx 72rpx; + box-sizing: border-box; + background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%); + overflow: hidden; +} + +.repair-page__glow { + position: absolute; + border-radius: 50%; + pointer-events: none; +} + +.repair-page__glow--left { + top: 60rpx; + left: -120rpx; + width: 320rpx; + height: 320rpx; + background: radial-gradient(circle, rgba(53, 229, 179, 0.18) 0%, rgba(53, 229, 179, 0) 72%); +} + +.repair-page__glow--right { + top: 260rpx; + right: -120rpx; + width: 340rpx; + height: 340rpx; + background: radial-gradient(circle, rgba(76, 118, 214, 0.14) 0%, rgba(76, 118, 214, 0) 72%); +} + +.repair-page__toolbar, +.repair-page__tabs, +.repair-page__card, +.repair-page__add-card, +.repair-page__submit { + position: relative; + z-index: 1; +} + +.repair-page__toolbar { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24rpx; +} + +.repair-page__toolbar-spacer { + width: 1rpx; + height: 1rpx; +} + +.repair-page__history { + display: flex; + align-items: center; + gap: 12rpx; + padding: 8rpx 0 8rpx 12rpx; +} + +.repair-page__clock { + position: relative; + width: 34rpx; + height: 34rpx; + border: 2rpx solid rgba(255, 255, 255, 0.8); + border-radius: 50%; +} + +.repair-page__clock-hand { + position: absolute; + left: 50%; + bottom: 50%; + width: 2rpx; + background: rgba(255, 255, 255, 0.8); + border-radius: 999rpx; + transform-origin: bottom center; +} + +.repair-page__clock-hand--hour { + height: 9rpx; + transform: translateX(-50%) rotate(18deg); +} + +.repair-page__clock-hand--minute { + height: 12rpx; + transform: translateX(-50%) rotate(112deg); +} + +.repair-page__history-text { + color: rgba(255, 255, 255, 0.76); + font-size: 22rpx; +} + +.repair-page__tabs { + display: flex; + gap: 24rpx; + margin-bottom: 24rpx; +} + +.repair-page__tab { + min-width: 218rpx; + padding: 24rpx 28rpx; + border-radius: 999rpx; + background: rgba(34, 40, 57, 0.88); + text-align: center; + box-sizing: border-box; +} + +.repair-page__tab--active { + background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%); + box-shadow: 0 18rpx 38rpx rgba(24, 178, 156, 0.24); +} + +.repair-page__tab-text { + color: rgba(255, 255, 255, 0.72); + font-size: 26rpx; +} + +.repair-page__tab-text--active { + color: #ffffff; + font-weight: 600; +} + +.repair-page__card, +.repair-page__add-card { + border-radius: 28rpx; + background: rgba(34, 40, 57, 0.94); + box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.03); +} + +.repair-page__card { + padding: 28rpx; +} + +.repair-page__card--contact { + margin-top: 28rpx; +} + +.repair-page__field-row { + display: flex; + align-items: center; + gap: 22rpx; +} + +.repair-page__field-row + .repair-page__field-row { + margin-top: 26rpx; +} + +.repair-page__field-label { + width: 124rpx; + flex-shrink: 0; + color: rgba(255, 255, 255, 0.9); + font-size: 26rpx; +} + +.repair-page__field-box { + display: flex; + align-items: center; + width: 0; + min-height: 84rpx; + flex: 1; + padding: 0 24rpx; + border-radius: 22rpx; + background: rgba(17, 24, 39, 0.96); + box-sizing: border-box; +} + +.repair-page__field-box--select { + justify-content: space-between; +} + +.repair-page__field-box--disabled { + opacity: 0.8; +} + +.repair-page__field-value { + color: #f7fbff; + font-size: 26rpx; +} + +.repair-page__field-value--muted { + color: rgba(255, 255, 255, 0.42); +} + +.repair-page__chevron { + width: 16rpx; + height: 16rpx; + border-right: 2rpx solid rgba(255, 255, 255, 0.68); + border-bottom: 2rpx solid rgba(255, 255, 255, 0.68); + transform: rotate(45deg); +} + +.repair-page__textarea-wrap { + margin-top: 28rpx; + padding: 24rpx 24rpx 58rpx; + border-radius: 24rpx; + background: rgba(17, 24, 39, 0.96); +} + +.repair-page__textarea { + width: 100%; + height: 180rpx; + color: #f5f7fb; + font-size: 28rpx; + line-height: 1.6; +} + +.repair-page__placeholder { + color: rgba(255, 255, 255, 0.32); +} + +.repair-page__textarea-count { + position: absolute; + right: 52rpx; + margin-top: 12rpx; + color: rgba(255, 255, 255, 0.34); + font-size: 22rpx; +} + +.repair-page__upload-panel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 340rpx; + margin-top: 26rpx; + padding: 32rpx; + border-radius: 28rpx; + background: rgba(17, 24, 39, 0.92); +} + +.repair-page__camera { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 28rpx; +} + +.repair-page__camera-top { + width: 26rpx; + height: 10rpx; + margin-right: 52rpx; + border-radius: 10rpx 10rpx 0 0; + background: rgba(255, 255, 255, 0.4); +} + +.repair-page__camera-body { + display: flex; + align-items: center; + justify-content: center; + width: 86rpx; + height: 58rpx; + border-radius: 16rpx; + background: rgba(255, 255, 255, 0.4); +} + +.repair-page__camera-lens { + width: 28rpx; + height: 28rpx; + border: 6rpx solid rgba(17, 24, 39, 0.9); + border-radius: 50%; +} + +.repair-page__upload-title { + color: rgba(255, 255, 255, 0.56); + font-size: 28rpx; + text-align: center; + line-height: 1.5; +} + +.repair-page__upload-subtitle { + margin-top: 12rpx; + color: rgba(255, 255, 255, 0.32); + font-size: 22rpx; +} + +.repair-page__attachment-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18rpx; + margin-top: 22rpx; +} + +.repair-page__attachment { + position: relative; + overflow: hidden; + border-radius: 22rpx; + background: rgba(12, 19, 31, 0.86); +} + +.repair-page__attachment-image, +.repair-page__video-cover { + display: block; + width: 100%; + height: 180rpx; +} + +.repair-page__video-cover { + position: relative; + background: linear-gradient(135deg, rgba(32, 201, 191, 0.18), rgba(67, 100, 247, 0.18)); +} + +.repair-page__video-badge { + position: absolute; + left: 18rpx; + bottom: 18rpx; + padding: 6rpx 12rpx; + border-radius: 999rpx; + background: rgba(11, 18, 32, 0.74); + color: #ffffff; + font-size: 20rpx; + letter-spacing: 1rpx; +} + +.repair-page__attachment-meta { + padding: 16rpx 18rpx 18rpx; +} + +.repair-page__attachment-name, +.repair-page__attachment-size { display: block; } + +.repair-page__attachment-name { + color: rgba(255, 255, 255, 0.88); + font-size: 22rpx; + line-height: 1.5; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.repair-page__attachment-size { + margin-top: 4rpx; + color: rgba(255, 255, 255, 0.34); + font-size: 20rpx; +} + +.repair-page__attachment-delete { + position: absolute; + top: 12rpx; + right: 12rpx; + display: flex; + align-items: center; + justify-content: center; + width: 42rpx; + height: 42rpx; + border-radius: 50%; + background: rgba(11, 18, 32, 0.75); +} + +.repair-page__attachment-delete-text { + color: #ffffff; + font-size: 28rpx; + line-height: 1; +} + +.repair-page__add-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + min-height: 124rpx; + margin-top: 28rpx; +} + +.repair-page__plus { + position: relative; + width: 54rpx; + height: 54rpx; + border-radius: 50%; + background: rgba(18, 201, 191, 0.18); +} + +.repair-page__plus-line { + position: absolute; + top: 50%; + left: 50%; + background: #22d3c5; + transform: translate(-50%, -50%); +} + +.repair-page__plus-line--horizontal { + width: 24rpx; + height: 2rpx; +} + +.repair-page__plus-line--vertical { + width: 2rpx; + height: 24rpx; +} + +.repair-page__add-text { + color: rgba(255, 255, 255, 0.38); + font-size: 22rpx; +} + +.repair-page__input { + width: 100%; + color: #f7fbff; + font-size: 26rpx; +} + +.repair-page__submit { + height: 96rpx; + margin-top: 44rpx; + border: none; + border-radius: 999rpx; + background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%); + color: #ffffff; + font-size: 32rpx; + font-weight: 600; + line-height: 96rpx; + box-shadow: 0 24rpx 42rpx rgba(24, 178, 156, 0.22); +} + +.repair-page__submit::after { + border: none; +} diff --git a/src/pages/repair/index.tsx b/src/pages/repair/index.tsx index 1e7dd2e..e2551de 100644 --- a/src/pages/repair/index.tsx +++ b/src/pages/repair/index.tsx @@ -1,28 +1,387 @@ -import SecondaryPage, { type SecondaryPageSection } from "../../components/secondary-page"; +import { Button, Image, Input, Text, Textarea, View } from "@tarojs/components"; +import Taro from "@tarojs/taro"; +import { useMemo, useState } from "react"; +import { + REPAIR_DESCRIPTION_LIMIT, + REPAIR_DEVICE_TYPE_LABELS, + REPAIR_DRAFT_STORAGE_KEY, + type RepairAttachment, + type RepairDeviceOption, + type RepairDeviceType, + type RepairSubmissionSnapshot, + appendRepairAttachments, + buildDeviceMap, + clampDescription, + normalizeChosenFiles, + validateRepairForm +} from "./repair-utils"; import "./index.scss"; -const sections: SecondaryPageSection[] = [ - { - title: "报修申请", - items: [ - { label: "提交报修申请" }, - { label: "上传故障图片", value: "待接入上传能力" }, - { label: "填写设备问题", value: "待接入表单" } - ] - }, - { - title: "售后进度", - items: [{ label: "维修进度查询", value: "后续接接口" }] - } +const repairDevices: RepairDeviceOption[] = [ + { id: "A9876456546", type: "monitor", label: "A9876456546", params: "3AW1 / 654616313" }, + { id: "A3648201488", type: "monitor", label: "A3648201488", params: "5BZ2 / 882730114" }, + { id: "C7842037781", type: "camera", label: "C7842037781", params: "AI-CAM / 220184900" }, + { id: "C7842037782", type: "camera", label: "C7842037782", params: "AI-CAM / 220184901" } ]; +const groupedDevices = buildDeviceMap(repairDevices); +const deviceTypeOrder: RepairDeviceType[] = ["monitor", "camera"]; + +function delay(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +function formatAttachmentSize(size?: number) { + if (!size) { + return ""; + } + + if (size >= 1024 * 1024) { + return `${(size / 1024 / 1024).toFixed(1)}MB`; + } + + return `${Math.max(size / 1024, 0.1).toFixed(1)}KB`; +} + +function createTicketNo() { + return `BX${Date.now().toString().slice(-8)}`; +} + export default function RepairPage() { + const [deviceType, setDeviceType] = useState("monitor"); + const [selectedDeviceId, setSelectedDeviceId] = useState(groupedDevices.monitor[0]?.id || ""); + const [description, setDescription] = useState(""); + const [attachments, setAttachments] = useState([]); + const [contactName, setContactName] = useState("张小龙"); + const [phone, setPhone] = useState("13689569989"); + const [submitting, setSubmitting] = useState(false); + + const currentDevices = groupedDevices[deviceType]; + const selectedDevice = + currentDevices.find((item) => item.id === selectedDeviceId) || currentDevices[0] || repairDevices[0]; + const imageCount = attachments.filter((item) => item.kind === "image").length; + const videoCount = attachments.filter((item) => item.kind === "video").length; + const uploadHint = useMemo(() => { + if (deviceType === "camera") { + return "点击上传AI摄像头故障照片或视频"; + } + + return "点击上传体征设备故障照片或视频"; + }, [deviceType]); + + const showToast = (title: string) => { + Taro.showToast({ + title, + icon: "none" + }); + }; + + const handleTypeChange = (nextType: RepairDeviceType) => { + if (nextType === deviceType) { + return; + } + + const nextDevices = groupedDevices[nextType]; + setDeviceType(nextType); + setSelectedDeviceId(nextDevices[0]?.id || ""); + }; + + const handleSelectDevice = async () => { + if (!currentDevices.length) { + showToast("当前暂无可选设备"); + return; + } + + try { + const result = await Taro.showActionSheet({ + itemList: currentDevices.map((item) => item.label) + }); + + const nextDevice = currentDevices[result.tapIndex]; + + if (nextDevice) { + setSelectedDeviceId(nextDevice.id); + } + } catch (error) { + const message = error instanceof Error ? error.message : ""; + + if (!message.includes("cancel")) { + showToast("设备选择失败"); + } + } + }; + + const handleChooseMedia = async () => { + try { + const result = await Taro.chooseMedia({ + count: 9, + mediaType: ["image", "video"], + sourceType: ["album", "camera"] + }); + + const chosen = normalizeChosenFiles((result.tempFiles || []) as never[]); + + if (!chosen.length) { + return; + } + + const merged = appendRepairAttachments(attachments, chosen); + setAttachments(merged.attachments); + + if (merged.errorMessage) { + showToast(merged.errorMessage); + } + } catch (error) { + const message = error instanceof Error ? error.message : ""; + + if (message.includes("cancel")) { + return; + } + + showToast("上传失败,请重试"); + } + }; + + const handlePreviewAttachment = (attachment: RepairAttachment) => { + if (attachment.kind === "image") { + Taro.previewImage({ + current: attachment.path, + urls: attachments.filter((item) => item.kind === "image").map((item) => item.path) + }); + return; + } + + const previewMedia = (Taro as unknown as { + previewMedia?: (options: { + current?: number; + sources: Array<{ url: string; type: "image" | "video"; poster?: string }>; + }) => Promise; + }).previewMedia; + + if (typeof previewMedia === "function") { + void previewMedia({ + current: 0, + sources: [ + { + url: attachment.path, + type: "video", + poster: attachment.thumbPath + } + ] + }); + return; + } + + showToast("当前环境暂不支持视频预览"); + }; + + const handleDeleteAttachment = (attachmentId: string, event: { stopPropagation?: () => void }) => { + event.stopPropagation?.(); + setAttachments((prev) => prev.filter((item) => item.id !== attachmentId)); + }; + + const handleSubmit = async () => { + const errorMessage = validateRepairForm({ + selectedDeviceId, + description, + contactName, + phone + }); + + if (errorMessage) { + showToast(errorMessage); + return; + } + + if (!selectedDevice) { + showToast("请选择设备"); + return; + } + + const payload: RepairSubmissionSnapshot = { + ticketNo: createTicketNo(), + deviceType, + deviceTypeLabel: REPAIR_DEVICE_TYPE_LABELS[deviceType], + deviceId: selectedDevice.id, + deviceParams: selectedDevice.params, + description: description.trim(), + attachments, + contactName: contactName.trim(), + phone: phone.trim(), + createdAt: new Date().toLocaleString("zh-CN", { hour12: false }), + statusLabel: "已提交,待审核" + }; + + setSubmitting(true); + + try { + await delay(800); + Taro.setStorageSync(REPAIR_DRAFT_STORAGE_KEY, payload); + await Taro.showToast({ + title: "提交成功", + icon: "success" + }); + await Taro.navigateTo({ url: "/pages/repair-detail/index" }); + } catch { + showToast("提交失败,请稍后重试"); + } finally { + setSubmitting(false); + } + }; + return ( - + + + + + + + + showToast("历史记录功能待开放")}> + + + + + 历史记录 + + + + + {deviceTypeOrder.map((item) => ( + handleTypeChange(item)} + > + + {REPAIR_DEVICE_TYPE_LABELS[item]} + + + ))} + + + + + 设备ID + + {selectedDevice?.label || "请选择设备"} + + + + + + 设备参数 + + {selectedDevice?.params || "--"} + + + + +