diff --git a/src/pages/repair/EventDetail.tsx b/src/pages/repair/EventDetail.tsx index 65d2bd1..10e8019 100644 --- a/src/pages/repair/EventDetail.tsx +++ b/src/pages/repair/EventDetail.tsx @@ -1,10 +1,12 @@ import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react" import type { components } from "../../types/saturday" import { saturdayClient } from "../../utils/client" +import { makeLogtoClient } from "../../utils/auth" import { Textarea, Input, Chip, Skeleton } from "@heroui/react" import type { PublicMember } from "../../store/member" import dayjs from "dayjs" import { EventStatus, UserEventAction } from "../../types/event" +import EventPhotos from "./EventPhotos" type PublicEvent = components["schemas"]["PublicEvent"] type Event = components["schemas"]["Event"] @@ -105,6 +107,7 @@ const EventDetail = forwardRef React.ReactNode token?: string currentMemberId?: string + isAdmin?: boolean }>((props, ref) => { const [event, setEvent] = useState() const [eventWithContact, setEventWithContact] = useState() @@ -125,16 +128,17 @@ const EventDetail = forwardRef { return event?.logs.findLast(v => v.action == "commit" || v.action == "alterCommit")?.description }, [event]) @@ -245,6 +253,17 @@ const EventDetail = forwardRef } + { + eventWithContact && (canUpload || !!eventWithContact.photos?.length) && ( + fetchContactInfo(eventWithContact.eventId)} + /> + ) + }
维修记录 diff --git a/src/pages/repair/EventPhotos.tsx b/src/pages/repair/EventPhotos.tsx new file mode 100644 index 0000000..1980e15 --- /dev/null +++ b/src/pages/repair/EventPhotos.tsx @@ -0,0 +1,131 @@ +import { useRef, useState } from "react" +import { Button } from "@heroui/react" +import type { components } from "../../types/saturday" +import { saturdayClient } from "../../utils/client" +import { makeLogtoClient } from "../../utils/auth" + +type Event = components["schemas"]["Event"] +type EventPhoto = components["schemas"]["EventPhoto"] + +const reasons: Record = { + "only the assigned member or an admin can add photos": "只有接单成员或管理员可以添加照片", + "photos can only be added to accepted or committed events": "工单当前的状态不能再添加照片", + "an event can have at most 20 photos": "每单最多 20 张照片", +} + +async function compress(file: File): Promise { + const bitmap = await createImageBitmap(file) + const scale = Math.min(1, 2048 / Math.max(bitmap.width, bitmap.height)) + const canvas = document.createElement("canvas") + canvas.width = Math.round(bitmap.width * scale) + canvas.height = Math.round(bitmap.height * scale) + canvas.getContext("2d")!.drawImage(bitmap, 0, 0, canvas.width, canvas.height) + bitmap.close() + const encode = (type: string, quality: number) => + new Promise(resolve => canvas.toBlob(resolve, type, quality)) + const webp = await encode("image/webp", 0.8) + if (webp?.type === "image/webp") return webp + const jpeg = await encode("image/jpeg", 0.85) + if (!jpeg) throw new Error("encode failed") + return jpeg +} + +export default function EventPhotos(props: { + photos: EventPhoto[] + eventId: number + canUpload: boolean + onUploaded: (event: Event) => void + onExpired: () => void +}) { + const input = useRef(null) + const refreshed = useRef(false) + const [uploading, setUploading] = useState(false) + const [error, setError] = useState("") + + const upload = async (files: FileList) => { + setError("") + if (props.photos.length + files.length > 20) { + setError("每单最多 20 张照片") + if (input.current) input.current.value = "" + return + } + setUploading(true) + try { + const blobs: Blob[] = [] + try { + for (const file of files) blobs.push(await compress(file)) + } + catch { + setError("有照片无法读取,请换一张再试") + return + } + const { data, error, response } = await saturdayClient.POST("/member/events/{EventId}/photos", { + params: { + path: { EventId: props.eventId }, + header: { Authorization: `Bearer ${await makeLogtoClient().getAccessToken()}` }, + }, + body: { photos: [] }, + bodySerializer: () => { + const form = new FormData() + blobs.forEach((blob, i) => form.append("photos", blob, `${i}.${blob.type === "image/webp" ? "webp" : "jpg"}`)) + return form + }, + }) + if (data) props.onUploaded(data) + else if (response.status === 422) setError(`上传失败:${reasons[error?.detail ?? ""] ?? "照片没有通过检查,请换一张再试"}`) + else setError("上传失败,请稍后再试") + } + catch { + setError("网络异常,上传失败,请稍后再试") + } + finally { + setUploading(false) + if (input.current) input.current.value = "" + } + } + + return ( +
+
+ 现场照片 + {props.canUpload && ( + <> + e.target.files?.length && upload(e.target.files)} + /> + + + )} +
+ {props.photos.length + ? ( + + ) + :

还没有照片

} + {error &&

{error}

} +
+ ) +} diff --git a/src/pages/repair/RepairAdmin.tsx b/src/pages/repair/RepairAdmin.tsx index f70f33c..f17c75b 100644 --- a/src/pages/repair/RepairAdmin.tsx +++ b/src/pages/repair/RepairAdmin.tsx @@ -151,6 +151,7 @@ function TicketDetailDrawer(props: { eventId={props.event?.eventId} token={props.identity?.token} currentMemberId={props.identity?.member?.memberId} + isAdmin={props.identity?.userInfo?.roles?.some(role => role.toLowerCase() === "repair admin")} > { event => ( diff --git a/src/pages/repair/TicketDetail.tsx b/src/pages/repair/TicketDetail.tsx index 2d38a71..f14ad4e 100644 --- a/src/pages/repair/TicketDetail.tsx +++ b/src/pages/repair/TicketDetail.tsx @@ -11,6 +11,7 @@ type PublicEvent = components["schemas"]["PublicEvent"] export default function TicketDetail() { const [userInfo, setUserInfo] = useState(null) + const [token, setToken] = useState() const [loading, setLoading] = useState(true) const [eventId, setEventId] = useState(null) const [event, setEvent] = useState(null) @@ -38,6 +39,7 @@ export default function TicketDetail() { if (authenticated) { const claims = await logtoClient.getIdTokenClaims() setUserInfo(claims) + setToken(await logtoClient.getAccessToken()) } else { // Redirect to login if not authenticated @@ -114,12 +116,12 @@ export default function TicketDetail() { ) } - if (!userInfo || !eventId) { + if (!userInfo || !eventId || !token) { return (

- {!userInfo ? "请先登录以查看维修详情" : "无效的维修单ID"} + {!userInfo || !token ? "请先登录以查看维修详情" : "无效的维修单ID"}