-
-
-
-
diff --git a/src/pages/repair/ticket-detail.astro b/src/pages/repair/ticket-detail.astro
index ef10f30..f079b8a 100644
--- a/src/pages/repair/ticket-detail.astro
+++ b/src/pages/repair/ticket-detail.astro
@@ -6,7 +6,7 @@ import EventDetail from "./EventDetail"
{SITE_TITLE}
- Email: {SITE_EMAIL}
+
+
-
+
-
+
+ {SITE_TITLE} By NBTCA
+
+
+ To be at the intersection of technology and liberal arts.
+
+
+
+ 联系我们:
+ {SITE_EMAIL}
+
-
- © {`2018-${year} ${SITE_NAME}`}
- astro-air-blog
-
-
- 浙ICP备2021030831号
+
diff --git a/src/components/header/NavigationUser.vue b/src/components/header/NavigationUser.vue
index e1ad240..0189eb2 100644
--- a/src/components/header/NavigationUser.vue
+++ b/src/components/header/NavigationUser.vue
@@ -13,6 +13,11 @@ const onSignIn = async () => {
postRedirectUri: window.location.pathname,
})
}
+
+const onGoToAccountManage = async () => {
+ window.open("https://myid.app.nbtca.space/account/aboutme", "_blank")
+}
+
const onSignOut = async () => {
logtoClient.value?.signOut(import.meta.env.PUBLIC_LOGTO_REDIRECT_URL)
}
@@ -66,10 +71,47 @@ onMounted(() => {
+
+
+
+ Copyright © {`${year} ${SITE_NAME}`}
+
+
+ 浙ICP备2021030831号
+
+
diff --git a/src/components/header/RepairHeader.astro b/src/components/header/RepairHeader.astro
index f6ae41d..a86df09 100644
--- a/src/components/header/RepairHeader.astro
+++ b/src/components/header/RepairHeader.astro
@@ -3,15 +3,34 @@ import NavigationUser from "./NavigationUser.vue"
---
diff --git a/src/consts.ts b/src/consts.ts
index 04709de..3335637 100644
--- a/src/consts.ts
+++ b/src/consts.ts
@@ -1,5 +1,5 @@
export const SITE_TITLE = `拔电关机`
export const SITE_EMAIL = "contact@nbtca.space"
-export const SITE_NAME = "Computer Association"
+export const SITE_NAME = "NingboTech University, Computer Association"
export const SITE_DESCRIPTION = "Computer Association"
export const SITE_URL = import.meta.env.PUBLIC_SITE_URL
diff --git a/src/pages/posts/PhoNBT2024.md b/src/pages/posts/PhoNBT2024.md
index c110aae..e401ce8 100644
--- a/src/pages/posts/PhoNBT2024.md
+++ b/src/pages/posts/PhoNBT2024.md
@@ -5,9 +5,7 @@ pubDate: 2024-06-18
description: ' “时间之河滚滚而去,青春韶华转瞬即逝”'
author: 'kongbai'
cover:
- url: 'https://oss.nbtca.space/blog/clas/YQL05614-5xNq3t-mid-ulLkGV.jpeg'
- square: 'https://www.apple.com.cn/newsroom/images/values/environment/Apple-Earth-Day-India-mangrove-Alibaug-canoe_Full-Bleed-Image.jpg.large_2x.jpg'
- alt: 'cover'
+ url: 'https://oss.nbtca.space/blog/clas/YQL05614-5xNq3t-mid-ulLkGV.jpeg'
tags: ["活动","志愿者", "新闻稿", "影留宁理","摄影","毕业"]
theme: 'white'
featured: true
diff --git a/src/pages/posts/_assets/workSummary/IMG_0069.jpeg b/src/pages/posts/_assets/workSummary/IMG_0069.jpeg
new file mode 100644
index 0000000..ddce41c
Binary files /dev/null and b/src/pages/posts/_assets/workSummary/IMG_0069.jpeg differ
diff --git a/src/pages/posts/_assets/workSummary/IMG_0069.jpg b/src/pages/posts/_assets/workSummary/IMG_0069.jpg
deleted file mode 100755
index 25c4b72..0000000
Binary files a/src/pages/posts/_assets/workSummary/IMG_0069.jpg and /dev/null differ
diff --git a/src/pages/posts/coldCarefulFun.md b/src/pages/posts/coldCarefulFun.md
index c6dbe5a..06046fa 100644
--- a/src/pages/posts/coldCarefulFun.md
+++ b/src/pages/posts/coldCarefulFun.md
@@ -5,7 +5,7 @@ pubDate: 2024-10-15
description: "活动的一些片段"
author: "kongbai"
cover:
- url: ./_assets/post20241016/free_huaji.jpg
+ url: https://oss.nbtca.space/blog/free_huaji_0F7NMW.jpg
alt: "cover"
tags: ["活动"]
---
diff --git a/src/pages/posts/workSummary.md b/src/pages/posts/workSummary.md
index f84e995..e809091 100644
--- a/src/pages/posts/workSummary.md
+++ b/src/pages/posts/workSummary.md
@@ -5,7 +5,7 @@ pubDate: 2025-01-12
description: ' 人类从历史中学到的唯一教训,就是人类无法从历史中学到任何教训'
author: 'zzh0u'
cover:
- url: ./_assets/workSummary/IMG_0069.jpg
+ url: https://oss.nbtca.space/blog/IMG_0069_8xFcDr.jpeg
tags: ["闲聊"]
theme: 'white'
featured: true
diff --git a/src/pages/repair/EventAction.tsx b/src/pages/repair/EventAction.tsx
new file mode 100644
index 0000000..950ee65
--- /dev/null
+++ b/src/pages/repair/EventAction.tsx
@@ -0,0 +1,313 @@
+import type { UserInfoResponse } from "@logto/browser"
+import type { PublicMember } from "../../store/member"
+import { EventStatus, type PublicEvent } from "../../types/event"
+import { saturdayApiBaseUrl } from "../../utils/client"
+import { Button, Form, Select, SelectItem, Textarea } from "@heroui/react"
+import { useEffect, useState } from "react"
+
+export type IdentityContext = {
+ member: PublicMember
+ userInfo: UserInfoResponse
+ token: string
+}
+
+enum RepairRole {
+ repairAdmin = "repair admin",
+ repairMember = "repair member",
+}
+
+export type EventActionProps = {
+ event: PublicEvent
+ identityContext: IdentityContext
+ isLoading?: string
+ onUpdated: (event: PublicEvent) => void
+ onLoading: (loadingAction?: string) => void
+}
+
+const EventSizeOptions: {
+ size: string
+ description?: string
+}[] = [
+ { size: "xs", description: "无需工具,仅简单排查或软件层级操作" },
+ { size: "s", description: "简单拆装部件,操作快,风险低" },
+ { size: "m", description: "需基本工具、一定技术判断,时间较长" },
+ { size: "l", description: "较复杂的拆装和测试流程,需熟练技能、多人协作可能" },
+ { size: "xl", description: "工作量极大,涉及多个设备,需团队作业和详细记录" },
+]
+
+const EventActionCommitForm = (props: {
+ formData: {
+ size: string
+ description: string
+ }
+ setFormData: (data: {
+ size: string
+ description: string
+ }) => void
+}) => {
+ const { formData, setFormData } = props
+ return (
+
+ )
+}
+
+export const EventActionCommit = (props: EventActionProps) => {
+ const [formData, setFormData] = useState({
+ size: "",
+ description: "",
+ })
+
+ useEffect(() => {
+ const description = props.event?.logs.findLast(v => v.action == "commit" || v.action == "alterCommit")?.description
+ setFormData({
+ size: props.event.size || "",
+ description: description || "",
+ })
+ }, [props.event])
+
+ const onSubmit = async () => {
+ props.onLoading("commit")
+ const res = await fetch(`${saturdayApiBaseUrl}/member/events/${props.event.eventId}/commit`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${props.identityContext.token}`,
+ ContentType: "application/json",
+ },
+ body: JSON.stringify({
+ size: formData.size,
+ content: formData.description,
+ }),
+ }).then(res => res.json())
+ props.onLoading()
+ return props.onUpdated(res)
+ }
+ return (
+
+type JsxHandler = (props: EventActionProps) => JSX.Element
+export type EventAction = {
+ action: string
+ label?: string
+ color?: "default" | "primary" | "secondary" | "success" | "warning" | "danger"
+ variant?: "flat" | "solid" | "bordered" | "light" | "faded" | "shadow" | "ghost"
+ handler?: CommonHandler
+ jsxHandler: JsxHandler
+}
+export const getAvailableEventActions = (event: PublicEvent, identityContext: IdentityContext) => {
+ console.log("getting event actions", event, identityContext)
+ const actions: EventAction[] = []
+
+ const makeCommonJsxHandler = (action: Omit) => {
+ return (props: EventActionProps) => {
+ const onAction = async (action: {
+ action: string
+ handler?: CommonHandler
+ }) => {
+ props.onLoading(action.action)
+ if (action.handler) {
+ const res = await action.handler()
+ props.onUpdated(res as PublicEvent)
+ }
+ props.onLoading()
+ }
+ return (
+
+
+
+
diff --git a/src/pages/repair/index.astro b/src/pages/repair/index.astro
index a3e3800..81ecbd9 100644
--- a/src/pages/repair/index.astro
+++ b/src/pages/repair/index.astro
@@ -16,6 +16,9 @@ import RepairHeader from "../../components/header/RepairHeader.astro"
const logtoClient = makeLogtoClient()
const createRepairPath = "/repair/create-ticket"
const authenticated = await logtoClient.isAuthenticated()
+ logtoClient.getIdTokenClaims().then((res) => {
+ console.log(res)
+ })
if (!authenticated) {
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
return
@@ -65,7 +68,7 @@ import RepairHeader from "../../components/header/RepairHeader.astro"
在接受预约之外,我们每月举办 理工维修日,定时定点提供维修。
+
+
+
+
+ )
+}
+export const EventActionAlterCommit = (props: EventActionProps) => {
+ const [formData, setFormData] = useState({
+ size: "",
+ description: "",
+ })
+ useEffect(() => {
+ const description = props.event?.logs?.findLast(v => v.action == "commit" || v.action == "alterCommit")?.description
+ setFormData({
+ size: props.event.size || "",
+ description: description || "",
+ })
+ }, [props.event])
+
+ const onSubmit = async () => {
+ props.onLoading("alterCommit")
+ const res = await fetch(`${saturdayApiBaseUrl}/member/events/${props.event.eventId}/commit`, {
+ method: "PATCH",
+ headers: {
+ Authorization: `Bearer ${props.identityContext.token}`,
+ ContentType: "application/json",
+ },
+ body: JSON.stringify({
+ size: formData.size,
+ content: formData.description,
+ }),
+ }).then(res => res.json())
+ props.onLoading()
+ return props.onUpdated(res)
+ }
+ return (
+
+
+
+
+
+ )
+}
+
+type CommonHandler = () => Promise
+
+
+ )
+ }
+ }
+
+ if (event.status == EventStatus.open) {
+ actions.push({
+ action: "accept",
+ jsxHandler: makeCommonJsxHandler({
+ action: "accept",
+ label: "接受",
+ variant: "solid",
+ color: "primary",
+ handler: async () => {
+ return await fetch(`${saturdayApiBaseUrl}/member/events/${event.eventId}/accept`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${identityContext.token}`,
+ },
+ }).then(res => res.json())
+ },
+ }),
+ })
+ }
+ else if (event.status == EventStatus.accepted && event.member?.memberId == identityContext.member.memberId) {
+ actions.push({
+ action: "commit",
+ jsxHandler: EventActionCommit,
+ })
+ actions.push({
+ action: "drop",
+ jsxHandler: makeCommonJsxHandler({
+ action: "drop",
+ label: "放弃",
+ handler: async () => {
+ return await fetch(`${saturdayApiBaseUrl}/member/events/${event.eventId}/accept`, {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${identityContext.token}`,
+ },
+ }).then(res => res.json())
+ },
+ }),
+ })
+ }
+ else if (event.status == EventStatus.committed) {
+ if (event.member?.memberId == identityContext.member.memberId) {
+ actions.push({
+ action: "alterCommit",
+ jsxHandler: EventActionAlterCommit,
+ })
+ }
+ if (identityContext.userInfo.roles.find(role => role.toLocaleLowerCase() == RepairRole.repairAdmin)) {
+ actions.push({
+ action: "close",
+ jsxHandler: makeCommonJsxHandler({
+ action: "close",
+ color: "success",
+ label: "完成",
+ handler: async () => {
+ return await fetch(`${saturdayApiBaseUrl}/events/${event.eventId}/close`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${identityContext.token}`,
+ },
+ }).then(res => res.json())
+ },
+ }),
+ })
+ actions.push({
+ action: "reject",
+ jsxHandler: makeCommonJsxHandler({
+ action: "rejectCommit",
+ color: "danger",
+ label: "退回",
+ handler: async () => {
+ return await fetch(`${saturdayApiBaseUrl}/events/${event.eventId}/commit`, {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${identityContext.token}`,
+ },
+ }).then(res => res.json())
+ },
+ }),
+ })
+ }
+ }
+
+ return actions
+}
diff --git a/src/pages/repair/EventDetail.tsx b/src/pages/repair/EventDetail.tsx
index 829f82b..9f4c36c 100644
--- a/src/pages/repair/EventDetail.tsx
+++ b/src/pages/repair/EventDetail.tsx
@@ -1,7 +1,7 @@
-import { useEffect, useState } from "react"
+import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"
import type { components } from "../../types/saturday"
import { saturdayClient } from "../../utils/client"
-import { Textarea, Input, Chip } from "@heroui/react"
+import { Textarea, Input, Chip, Skeleton } from "@heroui/react"
import type { PublicMember } from "../../store/member"
import dayjs from "dayjs"
import { EventStatus, UserEventAction } from "../../types/event"
@@ -15,8 +15,6 @@ function EventLogItem(props: {
}) {
return (
- {/*
- 未开始
+ return 未开始
case EventStatus.accepted:
- return 维修中
+ return 维修中
case EventStatus.committed:
- return 维修中
+ return 待审核
case EventStatus.closed:
- return 已完成
+ return 已完成
case EventStatus.cancelled:
- return 已取消
+ return 已取消
}
}
@@ -93,72 +93,133 @@ const filterEventLog = (event: PublicEvent) => {
}
return filteredLogs
}
-
-export default function EventDetail(props: {
- eventId?: number
-}) {
- const [event, setEvent] = useState()
- const fetchAndSetEvent = async (eventId: number) => {
- const { data } = await saturdayClient.GET("/events/{EventId}", {
- params: {
- path: {
- EventId: eventId,
- },
- },
- })
- setEvent(data)
- }
- useEffect(() => {
- const url = new URL(window.location.href)
- const eventId = props.eventId ?? url.searchParams.get("eventId")
- if (!eventId) {
- return
- }
- fetchAndSetEvent(eventId as unknown as number)
- }, [])
-
- return (
- event
- ? (
-
-
- )
- :
- )
+export type EventDetailRef = {
+ refresh: () => Promise
+ event: PublicEvent | undefined
}
+const EventDetail = forwardRef void
+ action?: React.ReactNode
+ children?: (event: PublicEvent) => React.ReactNode
+}>((props, ref) => {
+ const [event, setEvent] = useState()
+
+ const fetchAndSetEvent = async (eventId: number) => {
+ const { data } = await saturdayClient.GET("/events/{EventId}", {
+ params: {
+ path: {
+ EventId: eventId,
+ },
+ },
+ })
+ setEvent(data)
+ return data
+ }
+
+ const refresh = async () => {
+ const url = new URL(window.location.href)
+ const eventId = props.eventId ?? url.searchParams.get("eventId")
+ console.log("refresh eventId", eventId)
+ if (eventId) {
+ return await fetchAndSetEvent(eventId as unknown as number)
+ }
+ }
+
+ const repairDescription = useMemo(() => {
+ return event?.logs.findLast(v => v.action == "commit" || v.action == "alterCommit")?.description
+ }, [event])
+
+ // 初次加载
+ useEffect(() => {
+ refresh()
+ }, [])
+
+ // 暴露给父组件的方法
+ useImperativeHandle(ref, () => ({
+ refresh,
+ event,
+ }))
+
+ return (
+ event
+ ? (
+
+
+ )
+ : (
+
+
+ 导出为Excel
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/pages/repair/RepairAdmin.tsx b/src/pages/repair/RepairAdmin.tsx
new file mode 100644
index 0000000..b8f9d00
--- /dev/null
+++ b/src/pages/repair/RepairAdmin.tsx
@@ -0,0 +1,453 @@
+import {
+ Table,
+ TableHeader,
+ TableColumn,
+ TableBody,
+ TableRow,
+ TableCell,
+ User,
+ Pagination,
+ Spinner,
+ Popover,
+ PopoverTrigger,
+ PopoverContent,
+ Button,
+ CheckboxGroup,
+ Checkbox,
+ Drawer,
+ DrawerContent,
+ DrawerHeader,
+ DrawerBody,
+ DrawerFooter,
+ useDisclosure,
+ Chip,
+} from "@heroui/react"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import { useAsyncList } from "@react-stately/data"
+import type { components } from "../../types/saturday"
+import { saturdayApiBaseUrl, saturdayClient } from "../../utils/client"
+import EventDetail, { EventStatusChip, type EventDetailRef } from "./EventDetail"
+import dayjs from "dayjs"
+import { EventStatus, UserEventStatus } from "../../types/event"
+import { makeLogtoClient } from "../../utils/auth"
+import type { PublicMember } from "../../store/member"
+import type { UserInfoResponse } from "@logto/browser"
+import { getAvailableEventActions, type EventAction, type IdentityContext } from "./EventAction"
+import { ExportExcelModal } from "./ExportEventDialog"
+
+type PublicEvent = components["schemas"]["PublicEvent"]
+
+export const EyeIcon = (props) => {
+ return (
+
+ )
+}
+
+function CheckboxPopover(props: {
+ value: string[]
+ onValueChange: (value: string[]) => void
+}) {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+function TicketDetailDrawer(props: {
+ event: PublicEvent
+ identity: IdentityContext
+ isOpen: boolean
+ onEventUpdated: (event: PublicEvent) => void
+ onOpenChange: (isOpen: boolean) => void
+ onClose: () => void
+ onDelete: () => void
+ onEdit: () => void
+}) {
+ const { isOpen, onOpenChange, onClose } = props
+ const [isLoading, setIsLoading] = useState("")
+
+ const eventDetailRef = useRef(null)
+
+ const [availableActions, setAvailableActions] = useState([])
+
+ useEffect(() => {
+ if (!props.event || !props.identity?.member || !props.identity?.userInfo?.roles) {
+ return
+ }
+ setAvailableActions(getAvailableEventActions(props.event, props.identity))
+ }, [props.event, props.identity])
+
+ const onEventUpdated = async (event: PublicEvent) => {
+ props.onEventUpdated(event)
+ const res = await eventDetailRef.current?.refresh()
+ console.log("onEventUpdated", res)
+ if (!res || !props.identity?.member || !props.identity?.userInfo?.roles) {
+ return
+ }
+ setAvailableActions(getAvailableEventActions(res, props.identity))
+ }
+
+ return (
+
+
+
+
+
+
+ {
+ event => (
+
+
+
+
+
+
+
+ )
+}
+
+export const validateRepairRole = (roles: string[]) => {
+ const acceptableRoles = ["repair admin", "repair member"]
+ return roles.some(role => acceptableRoles.includes(role.toLowerCase()))
+}
+
+export default function App() {
+ const [isLoading, setIsLoading] = useState(true)
+ const [page, setPage] = useState(1)
+ const rowsPerPage = 10
+ const [statusFilter, setStatusFilter] = useState(
+ UserEventStatus.filter(v => v.status !== EventStatus.cancelled).map(v => v.status),
+ )
+ const { isOpen, onOpen, onOpenChange } = useDisclosure()
+ const [userInfo, setUserInfo] = useState()
+ const [currentMember, setCurrentMember] = useState()
+ const [token, setToken] = useState()
+
+ useEffect(() => {
+ const check = async () => {
+ const adminPath = "/repair/admin"
+ const authenticated = await makeLogtoClient().isAuthenticated()
+ if (!authenticated) {
+ window.location.href = `/repair/login-hint?redirectUrl=${adminPath}`
+ return
+ }
+ const res = await makeLogtoClient().getIdTokenClaims()
+ const token = await makeLogtoClient().getAccessToken()
+ setToken(token)
+ const hasRole = validateRepairRole(res.roles)
+ if (!hasRole) {
+ window.location.href = `/repair/login-hint?redirectUrl=${adminPath}`
+ return
+ }
+ setUserInfo(res)
+
+ const currentMember = await fetch(`${saturdayApiBaseUrl}/member`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }).then(res => res.json())
+ setCurrentMember(currentMember)
+ }
+ check()
+ }, [])
+
+ const list = useAsyncList({
+ async load() {
+ const { data } = await saturdayClient.GET("/events", {
+ params: {
+ query: {
+ order: "DESC",
+ offset: 0,
+ limit: 1000,
+ },
+ },
+ })
+
+ setIsLoading(false)
+
+ return {
+ items: data,
+ }
+ },
+ async sort({ items, sortDescriptor }) {
+ return {
+ items: items.sort((a, b) => {
+ const first = a[sortDescriptor.column]
+ const second = b[sortDescriptor.column]
+ let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1
+
+ if (sortDescriptor.direction === "descending") {
+ cmp *= -1
+ }
+
+ return cmp
+ }),
+ }
+ },
+ })
+
+ const filteredList = useMemo(() => {
+ if (statusFilter.length > 0) {
+ return list.items.filter(item => statusFilter.includes(item.status))
+ }
+ return list.items
+ }, [list, statusFilter])
+
+ const items = useMemo(() => {
+ const start = (page - 1) * rowsPerPage
+ const end = start + rowsPerPage
+
+ return filteredList.slice(start, end)
+ }, [filteredList, page, rowsPerPage])
+
+ const pages = useMemo(() => {
+ return Math.ceil(filteredList.length / rowsPerPage)
+ }, [filteredList, rowsPerPage])
+
+ useEffect(() => {
+ setPage(1)
+ }, [statusFilter])
+
+ const columns: {
+ key: string
+ label: string
+ allowSorting?: boolean
+ content?: JSX.Element
+ }[] = [
+ {
+ key: "eventId",
+ label: "单号",
+ },
+ {
+ key: "problem",
+ label: "问题描述",
+ },
+ {
+ key: "model",
+ label: "型号",
+ },
+ {
+ key: "size",
+ label: "工作量",
+ },
+ {
+ key: "memberId",
+ label: "处理人",
+ },
+ {
+ key: "gmtCreate",
+ label: "创建时间",
+ },
+ {
+ key: "status",
+ label: "状态",
+ content: (
+ ()
+ const onOpenEventDetail = (event: PublicEvent) => {
+ setActiveEvent(event)
+ onOpen()
+ }
+
+ const renderCell = useCallback((event: PublicEvent, columnKey: string | number) => {
+ const cellValue = event[columnKey]
+
+ switch (columnKey) {
+ case "problem":
+ return (
+
+ {event.member.alias}
+
+ )
+ : <>>
+ )
+ case "size":
+ return (
+ cellValue ? {"size:" + cellValue} : <>>
+ )
+ case "gmtCreate":
+ return (
+
+ {dayjs(cellValue).format("YYYY-MM-DD HH:mm")}
+
+ )
+ case "status":
+ return EventStatusChip({
+ status: cellValue,
+ size: "sm",
+ })
+ case "actions":
+ return (
+
+ )
+
+ default:
+ return cellValue
+ }
+ }, [])
+ return (
+
+ {
+ onOpenChange()
+ }}
+ onDelete={() => {}}
+ onEdit={() => {}}
+ >
+
+
+ )
+}
diff --git a/src/pages/repair/TicketForm.tsx b/src/pages/repair/TicketForm.tsx
index 038d05e..9c01c18 100644
--- a/src/pages/repair/TicketForm.tsx
+++ b/src/pages/repair/TicketForm.tsx
@@ -181,8 +181,9 @@ function TicketFormCreated(props: {
-
-
-
*/}
@@ -37,7 +35,7 @@ function EventLogItem(props: {
+
@@ -46,20 +44,22 @@ function EventLogItem(props: {
)
}
-function EventStatusChip(props: {
+export function EventStatusChip(props: {
+ size?: "sm" | "md" | "lg"
status: string
}) {
+ const size = props.size || "md"
switch (props.status) {
case EventStatus.open:
- return
{dayjs(props.eventLog.gmtCreate).format("YYYY-MM-DD HH:mm")}
-
- 维修详情
-
-
- #{event.eventId}
-
-
-
-
-
-
-
-
-
-
- )
- })
- }
-
-
- 维修记录
-
- {
- filterEventLog(event).map((v, index) => {
- return (
-
+
+ 维修详情
+
+
+ #{event.eventId}
+
+
+ {
+ event.size
+ ? {"size:" + event.size}
+ : <>>
+ }
+
+
+
+
+
+ {
+ repairDescription
+ ? (
+
+ )
+ : <>>
+ }
+
+
+
+ )
+ })
+ }
+
+
+ 维修记录
+
+ {
+ filterEventLog(event).map((v, index) => {
+ return (
+
+ {props.children(event)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ )
+ })
+
+export default EventDetail
diff --git a/src/pages/repair/ExportEventDialog.tsx b/src/pages/repair/ExportEventDialog.tsx
new file mode 100644
index 0000000..5c0d59b
--- /dev/null
+++ b/src/pages/repair/ExportEventDialog.tsx
@@ -0,0 +1,108 @@
+import { useState } from "react"
+import {
+ Modal,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+ Button,
+ DateRangePicker,
+} from "@heroui/react"
+import { parseDate } from "@internationalized/date"
+import { saturdayApiBaseUrl } from "../../utils/client"
+import { makeLogtoClient } from "../../utils/auth"
+import dayjs from "dayjs"
+
+export function ExportExcelModal() {
+ const [isOpen, setIsOpen] = useState(false)
+ const [dateRange, setDateRange] = useState({
+ start: parseDate(dayjs().subtract(1, "month").format("YYYY-MM-DD")),
+ end: parseDate(dayjs().format("YYYY-MM-DD")),
+ })
+ const [loading, setLoading] = useState(false)
+
+ const openModal = () => setIsOpen(true)
+ const closeModal = () => setIsOpen(false)
+
+ const downloadExcel = async () => {
+ if (!dateRange.start || !dateRange.end) return
+
+ setLoading(true)
+ try {
+ const start = dateRange.start.toString() // Format: 'YYYY-MM-DD'
+ const end = dateRange.end.toString()
+ const url = `${saturdayApiBaseUrl}/events/xlsx?start_time=${start}&end_time=${end}`
+
+ const token = await makeLogtoClient().getAccessToken()
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ })
+ if (!response.ok) throw new Error("Download failed")
+
+ // Extract filename from Content-Disposition header
+ const disposition = response.headers.get("Content-Disposition")
+ let filename = "export.xlsx" // Default filename
+ if (disposition && disposition.includes("filename=")) {
+ const filenameMatch = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
+ if (filenameMatch != null && filenameMatch[1]) {
+ filename = filenameMatch[1].replace(/['"]/g, "")
+ }
+ }
+
+ const blob = await response.blob()
+ const downloadUrl = window.URL.createObjectURL(blob)
+ const link = document.createElement("a")
+ link.href = downloadUrl
+ link.setAttribute("download", filename)
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ window.URL.revokeObjectURL(downloadUrl) // Clean up
+ }
+ catch (error) {
+ alert("Failed to download Excel file: " + error.message)
+ }
+ finally {
+ setLoading(false)
+ closeModal()
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+ {
+ UserEventStatus.map((status) => {
+ return (
+
+
+
+ )
+ })
+ }
+
+
+ 维修详情
+ {isLoading} +
+ {
+ availableActions?.map((action) => {
+ return (
+ {
+ setIsLoading(action)
+ }}
+ >
+
+ )
+ }) || <>>
+ }
+
+ )
+ }
+
+ 状态
+
+
+ ),
+ },
+ {
+ key: "actions",
+ label: "操作",
+ },
+ ]
+
+ const [activeEvent, setActiveEvent] = useState
+ {cellValue}
+
+ )
+ case "memberId":
+ return (
+ event.member
+ ? (
+
+
+ : <>>
+ }
+
+ 维修管理
+ {
+ userInfo?.roles?.find(v => v.toLowerCase() == "repair admin")
+ ?
+
+
+
+
+
+
)
}
diff --git a/src/pages/repair/admin.astro b/src/pages/repair/admin.astro
new file mode 100644
index 0000000..20a5e04
--- /dev/null
+++ b/src/pages/repair/admin.astro
@@ -0,0 +1,12 @@
+---
+import BaseLayout from "../../layouts/BaseLayout.astro"
+import RepairHeader from "../../components/header/RepairHeader.astro"
+import RepairAdmin from "./RepairAdmin"
+---
+
+
+
+
+
+
diff --git a/src/styles/global.css b/src/styles/global.css
index d1170cf..e05d037 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -7709,16 +7709,16 @@ html.no-touch.no-reduced-motion
}
.footer-main .item .logo {
- font-size: 16px;
+ /* font-size: 16px; */
font-weight: 600;
color: var(--black);
}
.footer-main .foot-nav-items {
- display: flex;
+ /* display: flex;
flex-direction: row;
justify-content: space-between;
- align-items: flex-start;
+ align-items: flex-start; */
}
@media screen and (min-width: 320px) {
@@ -7774,10 +7774,10 @@ html.no-touch.no-reduced-motion
}
.footer-main .foot-nav-items {
- display: flex;
+ /* display: flex;
flex-direction: row;
justify-content: space-between;
- align-items: flex-start;
+ align-items: flex-start; */
}
@media screen and (min-width: 320px) {
@@ -7805,7 +7805,7 @@ html.no-touch.no-reduced-motion
.footer-main .item div,
.footer-main .item a {
- padding: 8px 5px;
+ padding: 4px 0px;
font-size: 14px;
color: var(--footer-pipe-color);
}
@@ -7816,7 +7816,7 @@ html.no-touch.no-reduced-motion
.footer-main .foot-nav-items .item-title {
color: var(--footer-directory-title-color);
- font-size: 14px;
+ /* font-size: 14px; */
font-weight: 600;
}
diff --git a/src/types/event.ts b/src/types/event.ts
index 5f17fea..5b31042 100644
--- a/src/types/event.ts
+++ b/src/types/event.ts
@@ -1,3 +1,5 @@
+import type { components } from "./saturday"
+
export interface Status {
status: string
text: string
@@ -76,3 +78,5 @@ export const UserEventAction: Action[] = [
icon: "status_cancelled.svg",
},
]
+
+export type PublicEvent = components["schemas"]["PublicEvent"]
diff --git a/src/utils/client.ts b/src/utils/client.ts
index 858f920..f6c8919 100644
--- a/src/utils/client.ts
+++ b/src/utils/client.ts
@@ -2,8 +2,10 @@ import createClient from "openapi-fetch"
import type { paths as saturdayPaths } from "../types/saturday"
import { ApiClient } from "./active"
+export const saturdayApiBaseUrl = import.meta.env.PROD ? "https://api.nbtca.space/v2" : "/saturday"
+
export const saturdayClient = createClient({
- baseUrl: import.meta.env.PROD ? "https://api.nbtca.space/v2/" : "/saturday",
+ baseUrl: saturdayApiBaseUrl,
})
export const activeClient = new ApiClient({