388 lines
13 KiB
TypeScript
388 lines
13 KiB
TypeScript
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>
|
||
);
|
||
}
|