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 || "--"}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {uploadHint}
+
+ 图片 {imageCount}/9,视频 {videoCount}/1
+
+
+
+ {attachments.length ? (
+
+ {attachments.map((attachment) => (
+ handlePreviewAttachment(attachment)}
+ >
+ {attachment.kind === "image" ? (
+
+ ) : (
+
+ {attachment.thumbPath ? (
+
+ ) : null}
+ VIDEO
+
+ )}
+
+
+ {attachment.name || "附件"}
+
+ {attachment.kind === "video" ? `视频 ${formatAttachmentSize(attachment.size)}` : formatAttachmentSize(attachment.size)}
+
+
+
+ handleDeleteAttachment(attachment.id, event)}>
+ ×
+
+
+ ))}
+
+ ) : null}
+
+
+
+
+
+
+
+ 继续添加附件
+
+
+
+
+ 联系人
+
+ setContactName(event.detail.value)}
+ />
+
+
+
+
+ 手机号
+
+ setPhone(event.detail.value)}
+ />
+
+
+
+
+
+
);
}
diff --git a/src/pages/repair/repair-utils.ts b/src/pages/repair/repair-utils.ts
new file mode 100644
index 0000000..37ab740
--- /dev/null
+++ b/src/pages/repair/repair-utils.ts
@@ -0,0 +1,144 @@
+export type RepairDeviceType = "monitor" | "camera";
+
+export type RepairDeviceOption = {
+ id: string;
+ type: RepairDeviceType;
+ label: string;
+ params: string;
+};
+
+export type RepairAttachment = {
+ id: string;
+ kind: "image" | "video";
+ path: string;
+ size?: number;
+ duration?: number;
+ thumbPath?: string;
+ name?: string;
+};
+
+export type RepairFormInput = {
+ selectedDeviceId: string;
+ description: string;
+ contactName: string;
+ phone: string;
+};
+
+export type RepairSubmissionSnapshot = {
+ ticketNo: string;
+ deviceType: RepairDeviceType;
+ deviceTypeLabel: string;
+ deviceId: string;
+ deviceParams: string;
+ description: string;
+ attachments: RepairAttachment[];
+ contactName: string;
+ phone: string;
+ createdAt: string;
+ statusLabel: string;
+};
+
+type ChosenMediaFile = {
+ tempFilePath: string;
+ fileType?: "image" | "video";
+ size?: number;
+ duration?: number;
+ thumbTempFilePath?: string;
+};
+
+export const REPAIR_DESCRIPTION_LIMIT = 60;
+export const REPAIR_DRAFT_STORAGE_KEY = "repair-request-draft";
+export const REPAIR_DEVICE_TYPE_LABELS: Record = {
+ monitor: "体征监测设备",
+ camera: "AI摄像头"
+};
+
+export function clampDescription(value: string, limit = REPAIR_DESCRIPTION_LIMIT) {
+ return value.slice(0, limit);
+}
+
+export function isValidPhone(value: string) {
+ return /^1[3-9]\d{9}$/.test(value);
+}
+
+export function buildDeviceMap(devices: RepairDeviceOption[]) {
+ return devices.reduce>(
+ (result, item) => {
+ result[item.type].push(item);
+ return result;
+ },
+ {
+ monitor: [],
+ camera: []
+ }
+ );
+}
+
+export function normalizeChosenFiles(files: ChosenMediaFile[]): RepairAttachment[] {
+ return files.map((item, index) => ({
+ id: `${item.tempFilePath}-${index}`,
+ kind: item.fileType === "video" ? "video" : "image",
+ path: item.tempFilePath,
+ size: item.size,
+ duration: item.duration,
+ thumbPath: item.thumbTempFilePath,
+ name: item.tempFilePath.split("/").pop() || item.tempFilePath
+ }));
+}
+
+export function appendRepairAttachments(existing: RepairAttachment[], incoming: RepairAttachment[]) {
+ const attachments = [...existing];
+ let errorMessage = "";
+
+ incoming.forEach((item) => {
+ if (item.kind === "video") {
+ const hasVideo = attachments.some((attachment) => attachment.kind === "video");
+
+ if (hasVideo) {
+ errorMessage = errorMessage || "最多上传1个视频";
+ return;
+ }
+
+ attachments.push(item);
+ return;
+ }
+
+ const imageCount = attachments.filter((attachment) => attachment.kind === "image").length;
+
+ if (imageCount >= 9) {
+ errorMessage = errorMessage || "最多上传9张图片";
+ return;
+ }
+
+ attachments.push(item);
+ });
+
+ return {
+ attachments,
+ errorMessage
+ };
+}
+
+export function validateRepairForm(input: RepairFormInput) {
+ if (!input.selectedDeviceId) {
+ return "请选择设备";
+ }
+
+ if (!input.description.trim()) {
+ return "请填写问题描述";
+ }
+
+ if (!input.contactName.trim()) {
+ return "请填写联系人";
+ }
+
+ if (!input.phone.trim()) {
+ return "请填写手机号";
+ }
+
+ if (!isValidPhone(input.phone)) {
+ return "请输入正确的手机号";
+ }
+
+ return "";
+}
diff --git a/tsconfig.report-tests.json b/tsconfig.report-tests.json
index f094f83..77ef0a6 100644
--- a/tsconfig.report-tests.json
+++ b/tsconfig.report-tests.json
@@ -8,5 +8,5 @@
"declaration": false,
"types": []
},
- "include": ["src/pages/report/report-utils.ts"]
+ "include": ["src/pages/report/report-utils.ts", "src/pages/repair/repair-utils.ts"]
}