Files
taiheEhu/src/pages/repair/index.tsx
2026-05-08 15:09:41 +08:00

388 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 (
<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>
);
}