mirror of
https://github.com/m1ngsama/FUJI.git
synced 2025-12-25 02:56:38 +00:00
Merge branch 'main' into refactor-blog
This commit is contained in:
commit
a830ba1743
7 changed files with 681 additions and 504 deletions
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
|
|
@ -1,4 +1,7 @@
|
||||||
name: main
|
name: main
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
"@logto/browser": "^2.2.18",
|
"@logto/browser": "^2.2.18",
|
||||||
"@react-stately/data": "^3.13.0",
|
"@react-stately/data": "^3.13.0",
|
||||||
"@stylistic/eslint-plugin": "^2.13.0",
|
"@stylistic/eslint-plugin": "^2.13.0",
|
||||||
"astro": "^4.16.18",
|
"astro": "^4.16.19",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
"ical.js": "^2.1.0",
|
"ical.js": "^2.1.0",
|
||||||
|
|
|
||||||
924
pnpm-lock.yaml
924
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -6,10 +6,21 @@ import { SITE_TITLE, SITE_DESCRIPTION } from "../consts"
|
||||||
const { primaryTitle } = Astro.props
|
const { primaryTitle } = Astro.props
|
||||||
const title = primaryTitle ? `${primaryTitle} - ${SITE_TITLE}` : SITE_TITLE;
|
const title = primaryTitle ? `${primaryTitle} - ${SITE_TITLE}` : SITE_TITLE;
|
||||||
---
|
---
|
||||||
|
|
||||||
<html class="js no-touch progressive-image no-reduced-motion progressive" lang="zh-CN" dir="ltr">
|
<html class="js no-touch progressive-image no-reduced-motion progressive" lang="zh-CN" dir="ltr">
|
||||||
<head>
|
<head>
|
||||||
<BaseHead title={title} description={SITE_DESCRIPTION} />
|
<BaseHead title={title} description={SITE_DESCRIPTION} />
|
||||||
|
<script type="text/javascript" is:inline>
|
||||||
|
(function (c, l, a, r, i, t, y) {
|
||||||
|
c[a] = c[a] || function () {
|
||||||
|
(c[a].q = c[a].q || []).push(arguments)
|
||||||
|
}
|
||||||
|
t = l.createElement(r)
|
||||||
|
t.async = 1
|
||||||
|
t.src = "https://www.clarity.ms/tag/" + i
|
||||||
|
y = l.getElementsByTagName(r)[0]
|
||||||
|
y.parentNode.insertBefore(t, y)
|
||||||
|
})(window, document, "clarity", "script", "tcqhxia97o")
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="flex flex-col min-h-screen">
|
<body class="flex flex-col min-h-screen">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,39 @@ const extractScheduleEventsInRange = (
|
||||||
rangeEnd: ICAL.Time,
|
rangeEnd: ICAL.Time,
|
||||||
): ScheduleEvent[] => {
|
): ScheduleEvent[] => {
|
||||||
const vevents = icalComp.getAllSubcomponents("vevent")
|
const vevents = icalComp.getAllSubcomponents("vevent")
|
||||||
return vevents
|
|
||||||
|
// First, collect all exception events (events with RECURRENCE-ID)
|
||||||
|
const exceptions = new Map<string, Set<string>>() // uid -> set of recurrence-id strings
|
||||||
|
const exceptionEvents: ScheduleEvent[] = []
|
||||||
|
|
||||||
|
vevents.forEach((vevent) => {
|
||||||
|
const recurrenceId = vevent.getFirstPropertyValue("recurrence-id")
|
||||||
|
if (recurrenceId) {
|
||||||
|
const uid = vevent.getFirstPropertyValue("uid")
|
||||||
|
const event = new ICAL.Event(vevent)
|
||||||
|
|
||||||
|
// Check if exception is in range
|
||||||
|
if (event.startDate.compare(rangeEnd) <= 0 && event.endDate.compare(rangeStart) >= 0) {
|
||||||
|
exceptionEvents.push({
|
||||||
|
start: event.startDate.toJSDate(),
|
||||||
|
end: event.endDate.toJSDate(),
|
||||||
|
summary: event.summary,
|
||||||
|
description: event.description,
|
||||||
|
recurrenceId: recurrenceId.toString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track exception for filtering recurring events
|
||||||
|
if (!exceptions.has(uid)) {
|
||||||
|
exceptions.set(uid, new Set())
|
||||||
|
}
|
||||||
|
exceptions.get(uid)!.add(recurrenceId.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process regular and recurring events
|
||||||
|
const regularEvents = vevents
|
||||||
|
.filter(vevent => !vevent.getFirstPropertyValue("recurrence-id"))
|
||||||
.flatMap<ScheduleEvent>((vevent) => {
|
.flatMap<ScheduleEvent>((vevent) => {
|
||||||
const event = new ICAL.Event(vevent)
|
const event = new ICAL.Event(vevent)
|
||||||
if (!event.isRecurring()) {
|
if (!event.isRecurring()) {
|
||||||
|
|
@ -41,8 +73,11 @@ const extractScheduleEventsInRange = (
|
||||||
recurrenceId: undefined,
|
recurrenceId: undefined,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
return expandEventOccurrences(event, rangeStart, rangeEnd)
|
const eventExceptions = exceptions.get(event.uid) || new Set()
|
||||||
|
return expandEventOccurrences(event, rangeStart, rangeEnd, eventExceptions)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return [...regularEvents, ...exceptionEvents]
|
||||||
.sort((a, b) => b.start.getTime() - a.start.getTime())
|
.sort((a, b) => b.start.getTime() - a.start.getTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +85,7 @@ const expandEventOccurrences = (
|
||||||
event: ICAL.Event,
|
event: ICAL.Event,
|
||||||
rangeStart: ICAL.Time,
|
rangeStart: ICAL.Time,
|
||||||
rangeEnd: ICAL.Time,
|
rangeEnd: ICAL.Time,
|
||||||
|
exceptions?: Set<string>,
|
||||||
): ScheduleEvent[] => {
|
): ScheduleEvent[] => {
|
||||||
const occurrences: ScheduleEvent[] = []
|
const occurrences: ScheduleEvent[] = []
|
||||||
const iterator = event.iterator()
|
const iterator = event.iterator()
|
||||||
|
|
@ -58,6 +94,10 @@ const expandEventOccurrences = (
|
||||||
if (!next) break
|
if (!next) break
|
||||||
if (next.compare(rangeEnd) > 0) break
|
if (next.compare(rangeEnd) > 0) break
|
||||||
if (next.compare(rangeStart) < 0) continue
|
if (next.compare(rangeStart) < 0) continue
|
||||||
|
|
||||||
|
// Skip this occurrence if it has an exception
|
||||||
|
if (exceptions && exceptions.has(next.toString())) continue
|
||||||
|
|
||||||
const details = event.getOccurrenceDetails(next)
|
const details = event.getOccurrenceDetails(next)
|
||||||
occurrences.push({
|
occurrences.push({
|
||||||
start: details.startDate.toJSDate(),
|
start: details.startDate.toJSDate(),
|
||||||
|
|
@ -70,28 +110,6 @@ const expandEventOccurrences = (
|
||||||
return occurrences
|
return occurrences
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractScheduleEvents = (icalComp: ICAL.Component): ScheduleEvent[] => {
|
|
||||||
const vevents = icalComp.getAllSubcomponents("vevent")
|
|
||||||
const rangeEnd = ICAL.Time.fromDateString("2026-01-01")
|
|
||||||
|
|
||||||
return vevents
|
|
||||||
.flatMap<ScheduleEvent>((vevent) => {
|
|
||||||
const event = new ICAL.Event(vevent)
|
|
||||||
if (event.iterator().complete) {
|
|
||||||
return [{
|
|
||||||
start: event.startDate.toJSDate(),
|
|
||||||
end: event.endDate.toJSDate(),
|
|
||||||
summary: event.summary,
|
|
||||||
description: event.description,
|
|
||||||
recurrenceId: event.uid || event.startDate.toString(),
|
|
||||||
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
return expandEventOccurrences(event, rangeEnd)
|
|
||||||
})
|
|
||||||
.sort((a, b) => b.start.getTime() - a.start.getTime())
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTimePair = (s: Date, e: Date): string => {
|
const formatTimePair = (s: Date, e: Date): string => {
|
||||||
const start = dayjs(s)
|
const start = dayjs(s)
|
||||||
const end = dayjs(e)
|
const end = dayjs(e)
|
||||||
|
|
@ -142,14 +160,6 @@ export default function Schedule() {
|
||||||
}
|
}
|
||||||
}, [focusedDate])
|
}, [focusedDate])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true)
|
|
||||||
parseCal().then((icalComp) => {
|
|
||||||
setScheduledEvents(extractScheduleEvents(icalComp))
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const groupedEvents = useMemo(() => {
|
const groupedEvents = useMemo(() => {
|
||||||
const grouped = new Map<string, ScheduleEvent[]>()
|
const grouped = new Map<string, ScheduleEvent[]>()
|
||||||
scheduledEvents.forEach((event) => {
|
scheduledEvents.forEach((event) => {
|
||||||
|
|
|
||||||
|
|
@ -140,15 +140,15 @@ function TicketDetailDrawer(props: {
|
||||||
return (
|
return (
|
||||||
<Drawer isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Drawer isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<DrawerHeader>
|
<DrawerHeader className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2">
|
||||||
<h2 className="text-2xl font-bold">维修详情</h2>
|
<h2 className="text-xl sm:text-2xl font-bold">维修详情</h2>
|
||||||
{isLoading}
|
{isLoading && <span className="text-sm text-gray-500">{isLoading}</span>}
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
<DrawerBody>
|
<DrawerBody className="px-4 sm:px-6">
|
||||||
<EventDetail ref={eventDetailRef} eventId={props.event?.eventId}>
|
<EventDetail ref={eventDetailRef} eventId={props.event?.eventId}>
|
||||||
{
|
{
|
||||||
event => (
|
event => (
|
||||||
<div className="mb-12 flex flex-col gap-2">
|
<div className="mb-8 sm:mb-12 flex flex-col gap-3 sm:gap-2">
|
||||||
{
|
{
|
||||||
availableActions?.map((action) => {
|
availableActions?.map((action) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -171,8 +171,8 @@ function TicketDetailDrawer(props: {
|
||||||
}
|
}
|
||||||
</EventDetail>
|
</EventDetail>
|
||||||
</DrawerBody>
|
</DrawerBody>
|
||||||
<DrawerFooter>
|
<DrawerFooter className="px-4 sm:px-6">
|
||||||
<Button variant="flat" onPress={onClose}>
|
<Button variant="flat" onPress={onClose} className="w-full sm:w-auto">
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
</DrawerFooter>
|
</DrawerFooter>
|
||||||
|
|
@ -336,6 +336,52 @@ export default function App() {
|
||||||
onOpen()
|
onOpen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MobileEventCard = ({ event }: { event: PublicEvent }) => (
|
||||||
|
<button className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm" onClick={() => onOpenEventDetail(event)}>
|
||||||
|
|
||||||
|
<div className="mb-3 flex gap-2 items-center justify-between">
|
||||||
|
<div className="text font-medium text-gray-900 line-clamp-2">
|
||||||
|
{event.problem}
|
||||||
|
<span className="text font-medium text-gray-400 ml-1">#{event.eventId}</span>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<EventStatusChip status={event.status} size="sm" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-18">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-600">
|
||||||
|
<div className="">
|
||||||
|
{ dayjs(event.gmtCreate).format("YYYY-MM-DD HH:mm") }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{event.model && (
|
||||||
|
<div>
|
||||||
|
{event.model}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{ event.size && <Chip size="sm">size:{event.size}</Chip>}
|
||||||
|
</div>
|
||||||
|
{event.member && (
|
||||||
|
<div className="flex items-center gap-2 ">
|
||||||
|
<User
|
||||||
|
avatarProps={{ radius: "full", src: event.member.avatar, size: "sm" }}
|
||||||
|
name=""
|
||||||
|
classNames={{
|
||||||
|
base: "justify-start",
|
||||||
|
name: "text-sm",
|
||||||
|
description: "text-xs",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
const renderCell = useCallback((event: PublicEvent, columnKey: string | number) => {
|
const renderCell = useCallback((event: PublicEvent, columnKey: string | number) => {
|
||||||
const cellValue = event[columnKey]
|
const cellValue = event[columnKey]
|
||||||
|
|
||||||
|
|
@ -389,45 +435,89 @@ export default function App() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
return (
|
return (
|
||||||
<section className="box-border max-w-[1024px] mx-auto px-[22px] mb-24">
|
<section className="box-border max-w-full px-4 sm:px-6 lg:max-w-[1024px] lg:px-[22px] mx-auto mb-16 sm:mb-24">
|
||||||
<div className="mt-6 flex justify-between items-center">
|
<div className="mt-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
<div className="text-2xl font-bold">维修管理</div>
|
<div className="text-xl sm:text-2xl font-bold">维修管理</div>
|
||||||
{
|
{
|
||||||
userInfo?.roles?.find(v => v.toLowerCase() == "repair admin")
|
userInfo?.roles?.find(v => v.toLowerCase() == "repair admin")
|
||||||
? <ExportExcelModal></ExportExcelModal>
|
? <div className="w-full sm:w-auto"><ExportExcelModal></ExportExcelModal></div>
|
||||||
: <></>
|
: <></>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div className="my-8 flex flex-col gap-4">
|
<div className="my-8 flex flex-col gap-4">
|
||||||
<Table
|
{/* Mobile Cards Layout */}
|
||||||
aria-label="Example table with dynamic content"
|
<div className="block sm:hidden">
|
||||||
sortDescriptor={list.sortDescriptor}
|
{/* Filter Section for Mobile */}
|
||||||
onSortChange={list.sort}
|
<div className="mb-4 flex items-center gap-2">
|
||||||
bottomContent={(
|
<span className="text-sm font-medium">筛选状态:</span>
|
||||||
<div className="flex w-full justify-center">
|
<CheckboxPopover value={statusFilter} onValueChange={setStatusFilter} />
|
||||||
<Pagination
|
</div>
|
||||||
isCompact
|
|
||||||
showControls
|
{isLoading
|
||||||
showShadow
|
? (
|
||||||
color="secondary"
|
<div className="flex justify-center py-8">
|
||||||
page={page}
|
<Spinner label="Loading..." />
|
||||||
total={pages}
|
</div>
|
||||||
onChange={page => setPage(page)}
|
)
|
||||||
/>
|
: (
|
||||||
</div>
|
<div className="flex flex-col gap-4">
|
||||||
)}
|
{items.map(event => (
|
||||||
>
|
<MobileEventCard key={event.eventId} event={event} />
|
||||||
<TableHeader columns={columns}>
|
))}
|
||||||
{column => <TableColumn key={column.key} allowsSorting={column.allowSorting} children={column.content ?? <div>{column.label}</div>}></TableColumn>}
|
{items.length === 0 && (
|
||||||
</TableHeader>
|
<div className="text-center py-8 text-gray-500">
|
||||||
<TableBody isLoading={isLoading} items={items} loadingContent={<Spinner label="Loading..." />}>
|
暂无维修记录
|
||||||
{item => (
|
</div>
|
||||||
<TableRow key={item.eventId}>
|
)}
|
||||||
{columnKey => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
</div>
|
||||||
</TableRow>
|
)}
|
||||||
|
|
||||||
|
{/* Mobile Pagination */}
|
||||||
|
<div className="mt-6 flex justify-center">
|
||||||
|
<Pagination
|
||||||
|
isCompact
|
||||||
|
showControls
|
||||||
|
showShadow
|
||||||
|
color="secondary"
|
||||||
|
page={page}
|
||||||
|
total={pages}
|
||||||
|
onChange={page => setPage(page)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Table Layout */}
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<Table
|
||||||
|
aria-label="Example table with dynamic content"
|
||||||
|
sortDescriptor={list.sortDescriptor}
|
||||||
|
onSortChange={list.sort}
|
||||||
|
bottomContent={(
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<Pagination
|
||||||
|
isCompact
|
||||||
|
showControls
|
||||||
|
showShadow
|
||||||
|
color="secondary"
|
||||||
|
page={page}
|
||||||
|
total={pages}
|
||||||
|
onChange={page => setPage(page)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
>
|
||||||
</Table>
|
<TableHeader columns={columns}>
|
||||||
|
{column => <TableColumn key={column.key} allowsSorting={column.allowSorting} children={column.content ?? <div>{column.label}</div>}></TableColumn>}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody isLoading={isLoading} items={items} loadingContent={<Spinner label="Loading..." />}>
|
||||||
|
{item => (
|
||||||
|
<TableRow key={item.eventId}>
|
||||||
|
{columnKey => <TableCell>{renderCell(item, columnKey)}</TableCell>}
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TicketDetailDrawer
|
<TicketDetailDrawer
|
||||||
event={activeEvent}
|
event={activeEvent}
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,6 @@ progress {
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue