Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pages/repair/EventDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ const EventDetail = forwardRef<EventDetailRef, {
photos={eventWithContact.photos ?? []}
eventId={eventWithContact.eventId}
canUpload={canUpload}
onUploaded={setEventWithContact}
onChanged={setEventWithContact}
onExpired={() => fetchContactInfo(eventWithContact.eventId)}
/>
)
Expand Down
70 changes: 55 additions & 15 deletions src/pages/repair/EventPhotos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const reasons: Record<string, string> = {
"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 张照片",
"only the assigned member or an admin can remove photos": "只有接单成员或管理员可以撤回照片",
"photos can only be removed from accepted or committed events": "工单当前的状态不能再撤回照片",
}

async function compress(file: File): Promise<Blob> {
Expand All @@ -34,12 +36,13 @@ export default function EventPhotos(props: {
photos: EventPhoto[]
eventId: number
canUpload: boolean
onUploaded: (event: Event) => void
onChanged: (event: Event) => void
onExpired: () => void
}) {
const input = useRef<HTMLInputElement>(null)
const refreshed = useRef(false)
const [uploading, setUploading] = useState(false)
const [removing, setRemoving] = useState<number | null>(null)
const [error, setError] = useState("")

const upload = async (files: FileList) => {
Expand Down Expand Up @@ -71,7 +74,7 @@ export default function EventPhotos(props: {
return form
},
})
if (data) props.onUploaded(data)
if (data) props.onChanged(data)
else if (response.status === 422) setError(`上传失败:${reasons[error?.detail ?? ""] ?? "照片没有通过检查,请换一张再试"}`)
else if (response.status === 401) setError("登录已过期,请重新登录后再上传")
else if (response.status === 403) setError("你没有添加照片的权限")
Expand All @@ -86,6 +89,30 @@ export default function EventPhotos(props: {
}
}

const remove = async (photoId: number) => {
if (!window.confirm("确定撤回这张照片吗?")) return
setError("")
setRemoving(photoId)
try {
const { data, error, response } = await saturdayClient.DELETE("/member/events/{EventId}/photos/{PhotoId}", {
params: {
path: { EventId: props.eventId, PhotoId: photoId },
header: { Authorization: `Bearer ${await makeLogtoClient().getAccessToken()}` },
},
})
if (data) props.onChanged(data)
else if (response.status === 404) props.onExpired()
else if (response.status === 422) setError(`撤回失败:${reasons[error?.detail ?? ""] ?? "请稍后再试"}`)
else setError("撤回失败,请稍后再试")
}
catch {
setError("网络异常,撤回失败,请稍后再试")
}
finally {
setRemoving(null)
}
}

return (
<div className="bg-gray-100 rounded-xl text-sm px-3 py-2">
<div className="flex items-center justify-between mb-2">
Expand All @@ -110,19 +137,32 @@ export default function EventPhotos(props: {
? (
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2">
{props.photos.map(photo => (
<a key={photo.photoId} href={photo.url} target="_blank" rel="noreferrer">
<img
src={photo.url}
alt="现场照片"
loading="lazy"
className="aspect-square w-full object-cover rounded-lg"
onError={() => {
if (refreshed.current) return
refreshed.current = true
props.onExpired()
}}
/>
</a>
<div key={photo.photoId} className="relative">
<a href={photo.url} target="_blank" rel="noreferrer">
<img
src={photo.url}
alt="现场照片"
loading="lazy"
className="aspect-square w-full object-cover rounded-lg"
onError={() => {
if (refreshed.current) return
refreshed.current = true
props.onExpired()
}}
/>
</a>
{props.canUpload && (
<button
type="button"
aria-label="撤回照片"
disabled={removing === photo.photoId}
onClick={() => remove(photo.photoId)}
className="absolute top-1 right-1 size-6 rounded-full bg-black/60 text-white text-sm leading-6 disabled:opacity-50"
>
×
</button>
)}
</div>
))}
</div>
)
Expand Down
65 changes: 65 additions & 0 deletions src/types/saturday.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,23 @@ export interface paths {
patch?: never
trace?: never
}
"/member/events/{EventId}/photos/{PhotoId}": {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
get?: never
put?: never
post?: never
/** Delete an event photo */
delete: operations["delete-event-photo"]
options?: never
head?: never
patch?: never
trace?: never
}
"/member/notification-preferences": {
parameters: {
query?: never
Expand Down Expand Up @@ -1831,6 +1848,54 @@ export interface operations {
}
}
}
"delete-event-photo": {
parameters: {
query?: never
header?: {
/** @description Bearer token or JWT token */
Authorization?: string
}
path: {
/**
* @description Event ID
* @example 123
*/
EventId: number
/**
* @description Photo ID
* @example 7
*/
PhotoId: number
}
cookie?: never
}
requestBody?: never
responses: {
/** @description OK */
200: {
headers: {
"X-Limit"?: number | null
"X-Offset"?: number | null
"X-Page"?: number | null
"X-Total-Count"?: number | null
"X-Total-Pages"?: number | null
[name: string]: unknown
}
content: {
"application/json": components["schemas"]["Event"]
}
}
/** @description Error */
default: {
headers: {
[name: string]: unknown
}
content: {
"application/problem+json": components["schemas"]["ErrorModel"]
}
}
}
}
"get-notification-preferences": {
parameters: {
query?: never
Expand Down
Loading