feat: 实现申请报修页面

This commit is contained in:
czz
2026-05-08 14:59:31 +08:00
parent c42344e38a
commit 6d93440334
12 changed files with 1328 additions and 23 deletions

View File

@@ -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<RepairDeviceType>("monitor");
const [selectedDeviceId, setSelectedDeviceId] = useState(groupedDevices.monitor[0]?.id || "");
const [description, setDescription] = useState("");
const [attachments, setAttachments] = useState<RepairAttachment[]>([]);
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<unknown>;
}).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 (
<SecondaryPage
eyebrow="REPAIR"
title="设备报修"
description="这个页面先把报修流程的主要节点放齐,后面接图片上传和工单接口时改动会比较小。"
sections={sections}
/>
<View className="repair-page">
<View className="repair-page__glow repair-page__glow--left" />
<View className="repair-page__glow repair-page__glow--right" />
<View className="repair-page__toolbar">
<View className="repair-page__toolbar-spacer" />
<View className="repair-page__history" onClick={() => showToast("历史记录功能待开放")}>
<View className="repair-page__clock">
<View className="repair-page__clock-hand repair-page__clock-hand--hour" />
<View className="repair-page__clock-hand repair-page__clock-hand--minute" />
</View>
<Text className="repair-page__history-text"></Text>
</View>
</View>
<View className="repair-page__tabs">
{deviceTypeOrder.map((item) => (
<View
className={`repair-page__tab ${item === deviceType ? "repair-page__tab--active" : ""}`}
key={item}
onClick={() => handleTypeChange(item)}
>
<Text className={`repair-page__tab-text ${item === deviceType ? "repair-page__tab-text--active" : ""}`}>
{REPAIR_DEVICE_TYPE_LABELS[item]}
</Text>
</View>
))}
</View>
<View className="repair-page__card">
<View className="repair-page__field-row">
<Text className="repair-page__field-label">ID</Text>
<View className="repair-page__field-box repair-page__field-box--select" onClick={handleSelectDevice}>
<Text className="repair-page__field-value">{selectedDevice?.label || "请选择设备"}</Text>
<View className="repair-page__chevron" />
</View>
</View>
<View className="repair-page__field-row">
<Text className="repair-page__field-label"></Text>
<View className="repair-page__field-box repair-page__field-box--disabled">
<Text className="repair-page__field-value repair-page__field-value--muted">{selectedDevice?.params || "--"}</Text>
</View>
</View>
<View className="repair-page__textarea-wrap">
<Textarea
className="repair-page__textarea"
maxlength={REPAIR_DESCRIPTION_LIMIT}
placeholder="问题描述60个字以内"
placeholderClass="repair-page__placeholder"
value={description}
onInput={(event) => setDescription(clampDescription(event.detail.value))}
/>
<Text className="repair-page__textarea-count">
{description.length}/{REPAIR_DESCRIPTION_LIMIT}
</Text>
</View>
<View className="repair-page__upload-panel" onClick={handleChooseMedia}>
<View className="repair-page__camera">
<View className="repair-page__camera-top" />
<View className="repair-page__camera-body">
<View className="repair-page__camera-lens" />
</View>
</View>
<Text className="repair-page__upload-title">{uploadHint}</Text>
<Text className="repair-page__upload-subtitle">
{imageCount}/9 {videoCount}/1
</Text>
</View>
{attachments.length ? (
<View className="repair-page__attachment-grid">
{attachments.map((attachment) => (
<View
className={`repair-page__attachment ${attachment.kind === "video" ? "repair-page__attachment--video" : ""}`}
key={attachment.id}
onClick={() => handlePreviewAttachment(attachment)}
>
{attachment.kind === "image" ? (
<Image className="repair-page__attachment-image" mode="aspectFill" src={attachment.path} />
) : (
<View className="repair-page__video-cover">
{attachment.thumbPath ? (
<Image className="repair-page__attachment-image" mode="aspectFill" src={attachment.thumbPath} />
) : null}
<View className="repair-page__video-badge">VIDEO</View>
</View>
)}
<View className="repair-page__attachment-meta">
<Text className="repair-page__attachment-name">{attachment.name || "附件"}</Text>
<Text className="repair-page__attachment-size">
{attachment.kind === "video" ? `视频 ${formatAttachmentSize(attachment.size)}` : formatAttachmentSize(attachment.size)}
</Text>
</View>
<View className="repair-page__attachment-delete" onClick={(event) => handleDeleteAttachment(attachment.id, event)}>
<Text className="repair-page__attachment-delete-text">×</Text>
</View>
</View>
))}
</View>
) : null}
</View>
<View className="repair-page__add-card" onClick={handleChooseMedia}>
<View className="repair-page__plus">
<View className="repair-page__plus-line repair-page__plus-line--horizontal" />
<View className="repair-page__plus-line repair-page__plus-line--vertical" />
</View>
<Text className="repair-page__add-text"></Text>
</View>
<View className="repair-page__card repair-page__card--contact">
<View className="repair-page__field-row">
<Text className="repair-page__field-label"></Text>
<View className="repair-page__field-box">
<Input
className="repair-page__input"
maxlength={20}
placeholder="请输入联系人"
placeholderClass="repair-page__placeholder"
value={contactName}
onInput={(event) => setContactName(event.detail.value)}
/>
</View>
</View>
<View className="repair-page__field-row">
<Text className="repair-page__field-label"></Text>
<View className="repair-page__field-box">
<Input
className="repair-page__input"
maxlength={11}
placeholder="请输入手机号"
placeholderClass="repair-page__placeholder"
type="number"
value={phone}
onInput={(event) => setPhone(event.detail.value)}
/>
</View>
</View>
</View>
<Button className="repair-page__submit" disabled={submitting} loading={submitting} onClick={handleSubmit}>
</Button>
</View>
);
}