Merge pull request #107 from wen-templari/feature/repair-system-updates

Add comprehensive repair system functionality
This commit is contained in:
clas 2025-09-30 00:00:12 +08:00 committed by GitHub
commit 9cf31ebcc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 856 additions and 149 deletions

View file

@ -83,6 +83,18 @@ export default function App() {
</Link> </Link>
</NavbarMenuItem> </NavbarMenuItem>
))} ))}
<NavbarItem>
<Link
color="foreground"
href="https://github.com/nbtca"
target="_blank"
rel="noopener noreferrer"
className="flex items-center"
>
Github
<img src="/src/pages/_assets/github-mark.svg" alt="GitHub" className="w-6 h-6" />
</Link>
</NavbarItem>
</NavbarMenu> </NavbarMenu>
</Navbar> </Navbar>
) )

View file

@ -27,7 +27,9 @@ makeLogtoClient().getIdTokenClaims().then((claims) => {
<div class="h-full flex items-center justify-between text-lg max-w-[1024px] mx-auto px-[22px]"> <div class="h-full flex items-center justify-between text-lg max-w-[1024px] mx-auto px-[22px]">
<span id="repair-header" class="font-semibold select-none cursor-default">维修</span> <span id="repair-header" class="font-semibold select-none cursor-default">维修</span>
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center gap-2 mr-4 text-xs text-gray-400"> <div class="flex items-center gap-4 mr-4 text-xs text-gray-400 mt-[1px]">
<a class="text-gray-500 hover:text-gray-700 appearance-none cursor-pointer" href="/repair/create-ticket" style="text-decoration:none">预约维修</a>
<a class="text-gray-500 hover:text-gray-700 appearance-none cursor-pointer" href="/repair/history" style="text-decoration:none">维修记录</a>
<a class="hidden text-gray-500 hover:text-gray-700 appearance-none cursor-pointer" id="repair-admin" href="/repair/admin" style="text-decoration:none">维修管理</a> <a class="hidden text-gray-500 hover:text-gray-700 appearance-none cursor-pointer" id="repair-admin" href="/repair/admin" style="text-decoration:none">维修管理</a>
</div> </div>
<NavigationUser client:load /> <NavigationUser client:load />

View file

@ -0,0 +1,131 @@
import { useState, useEffect } from "react"
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Input, Textarea } from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import { makeLogtoClient } from "../../utils/auth"
import type { components } from "../../types/saturday"
type PublicEvent = components["schemas"]["PublicEvent"]
type UpdateClientEventInputBody = components["schemas"]["UpdateClientEventInputBody"]
interface EditRepairModalProps {
isOpen: boolean
onClose: () => void
event: PublicEvent
onSaved: () => void
}
export default function EditRepairModal({ isOpen, onClose, event, onSaved }: EditRepairModalProps) {
const [loading, setLoading] = useState(false)
const [formData, setFormData] = useState<UpdateClientEventInputBody>({
problem: "",
model: "",
phone: "",
qq: "",
})
useEffect(() => {
if (event) {
setFormData({
problem: event.problem || "",
model: event.model || "",
phone: event.phone || "",
qq: event.qq || "",
})
}
}, [event])
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
try {
const logtoToken = await makeLogtoClient().getAccessToken()
const { error } = await saturdayClient.PATCH("/client/events/{EventId}", {
params: {
path: {
EventId: event.eventId,
},
},
headers: {
Authorization: `Bearer ${logtoToken}`,
},
body: formData,
})
if (error) {
throw new Error("Failed to update event")
}
onSaved()
}
catch (err) {
console.error("Error updating event:", err)
// Could add error handling/toast here
}
finally {
setLoading(false)
}
}
const handleClose = () => {
if (!loading) {
onClose()
}
}
return (
<Modal isOpen={isOpen} onClose={handleClose} size="2xl" scrollBehavior="inside">
<ModalContent>
<form onSubmit={handleSubmit}>
<ModalHeader className="flex flex-col gap-1">
#{event?.eventId}
</ModalHeader>
<ModalBody>
<div className="flex flex-col gap-4">
<Textarea
label="问题描述"
placeholder="告诉我们你遇到的问题"
isRequired
value={formData.problem}
onChange={e => setFormData({ ...formData, problem: e.target.value })}
/>
<Input
label="设备型号"
placeholder="你的设备型号"
value={formData.model || ""}
onChange={e => setFormData({ ...formData, model: e.target.value })}
/>
<Input
label="电话号码"
placeholder="必填"
isRequired
type="tel"
maxLength={11}
value={formData.phone}
onChange={e => setFormData({ ...formData, phone: e.target.value })}
/>
<Input
label="QQ号码"
placeholder="你的QQ号"
value={formData.qq || ""}
onChange={e => setFormData({ ...formData, qq: e.target.value })}
/>
</div>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={handleClose} isDisabled={loading}>
</Button>
<Button color="primary" type="submit" isLoading={loading}>
</Button>
</ModalFooter>
</form>
</ModalContent>
</Modal>
)
}

View file

@ -199,7 +199,7 @@ const EventDetail = forwardRef<EventDetailRef, {
</div> </div>
</div> </div>
<div> <div>
{props.children(event)} {props.children && props.children(event)}
</div> </div>
</section> </section>
) )

View file

@ -0,0 +1,54 @@
import { Card, CardBody, Chip } from "@heroui/react"
import { EventStatusChip } from "./EventDetail"
import dayjs from "dayjs"
import type { components } from "../../types/saturday"
type PublicEvent = components["schemas"]["PublicEvent"]
interface RepairHistoryCardProps {
event: PublicEvent
onViewDetail: (event: PublicEvent) => void
}
export default function RepairHistoryCard({ event, onViewDetail }: RepairHistoryCardProps) {
const handleCardClick = () => {
onViewDetail(event)
}
return (
<Card
className="w-full cursor-pointer hover:bg-gray-50 transition shadow"
isPressable
onPress={handleCardClick}
>
<CardBody className="pb-2">
<div className="flex justify-between items-start mb-2">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">#{event.eventId}</span>
<EventStatusChip status={event.status} size="sm" />
{event.size && <Chip size="sm">{event.size}</Chip>}
</div>
<span className="text-xs text-gray-400">
{dayjs(event.gmtCreate).format("YYYY-MM-DD")}
</span>
</div>
<div className="mb-2">
<p className="text-sm font-medium line-clamp-2">{event.problem}</p>
{event.model && (
<p className="text-xs text-gray-500 mt-1">: {event.model}</p>
)}
</div>
<div className="text-xs text-gray-400">
{dayjs(event.gmtCreate).format("YYYY-MM-DD HH:mm")}
{event.gmtModified !== event.gmtCreate && (
<span className="ml-2">
{dayjs(event.gmtModified).format("MM-DD HH:mm")}
</span>
)}
</div>
</CardBody>
</Card>
)
}

View file

@ -0,0 +1,84 @@
import { useState, useEffect } from "react"
import { Button, Spinner } from "@heroui/react"
import { makeLogtoClient } from "../../utils/auth"
import UserRepairHistory from "./UserRepairHistory"
import type { UserInfoResponse } from "@logto/browser"
export default function RepairHistoryPage() {
const [userInfo, setUserInfo] = useState<UserInfoResponse | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
checkAuthStatus()
}, [])
const checkAuthStatus = async () => {
try {
const logtoClient = makeLogtoClient()
const authenticated = await logtoClient.isAuthenticated()
if (authenticated) {
const claims = await logtoClient.getIdTokenClaims()
setUserInfo(claims)
}
else {
// Redirect to login if not authenticated
window.location.href = "/repair/login-hint?redirectUrl=/repair/history"
}
}
catch (error) {
console.error("Error checking auth status:", error)
// Redirect to login on error
window.location.href = "/repair/login-hint?redirectUrl=/repair/history"
}
finally {
setLoading(false)
}
}
const handleLogin = () => {
makeLogtoClient().signIn({
redirectUri: import.meta.env.PUBLIC_LOGTO_CALLBACK_URL,
postRedirectUri: "/repair/history",
})
}
const handleCreateNew = () => {
window.location.href = "/repair/create-ticket"
}
if (loading) {
return (
<div className="container mx-auto pt-16 pb-20">
<div className="flex flex-col items-center justify-center">
<Spinner size="lg" />
</div>
</div>
)
}
if (!userInfo) {
return (
<div className="container mx-auto pt-16 pb-20">
<div className="flex flex-col items-center justify-center">
<p className="text-gray-500 mb-4"></p>
<Button color="primary" onPress={handleLogin}>
</Button>
</div>
</div>
)
}
return (
<div className="container mx-auto pt-6 pb-20">
{/* History content */}
<div className="section-content">
<UserRepairHistory
onLogin={handleLogin}
onCreateNew={handleCreateNew}
/>
</div>
</div>
)
}

View file

@ -0,0 +1,229 @@
import { useState, useEffect } from "react"
import { Button, Spinner, Card, CardBody, CardFooter } from "@heroui/react"
import { makeLogtoClient } from "../../utils/auth"
import { saturdayClient } from "../../utils/client"
import RepairHistoryCard from "./RepairHistoryCard"
import EditRepairModal from "./EditRepairModal"
import type { UserInfoResponse } from "@logto/browser"
import type { components } from "../../types/saturday"
// eslint-disable-next-line @cspell/spellchecker
import hayasaka from "../_assets/hayasaka.jpg"
type PublicEvent = components["schemas"]["PublicEvent"]
export default function RepairLandingSection() {
const [userInfo, setUserInfo] = useState<UserInfoResponse | null>(null)
const [loading, setLoading] = useState(true)
const [recentEvents, setRecentEvents] = useState<PublicEvent[]>([])
const [selectedEvent, setSelectedEvent] = useState<PublicEvent | null>(null)
const [isEditOpen, setIsEditOpen] = useState(false)
useEffect(() => {
checkAuthStatusAndFetchEvents()
}, [])
const checkAuthStatusAndFetchEvents = async () => {
try {
const logtoClient = makeLogtoClient()
const authenticated = await logtoClient.isAuthenticated()
if (authenticated) {
const claims = await logtoClient.getIdTokenClaims()
setUserInfo(claims)
await fetchRecentEvents()
}
}
catch (error) {
console.error("Error checking auth status:", error)
}
finally {
setLoading(false)
}
}
const fetchRecentEvents = async () => {
try {
const logtoToken = await makeLogtoClient().getAccessToken()
const { data } = await saturdayClient.GET("/client/events", {
headers: {
Authorization: `Bearer ${logtoToken}`,
},
params: {
query: {
limit: 10,
offset: 0,
},
},
})
if (data) {
// Filter for unfinished events and take only the first 3
const unfinishedEvents = (data as unknown as PublicEvent[]).slice(0, 3)
setRecentEvents(unfinishedEvents)
}
}
catch (error) {
console.error("Error fetching recent events:", error)
}
}
const handleCreateTicketClick = async () => {
const logtoClient = makeLogtoClient()
const createRepairPath = "/repair/create-ticket"
const authenticated = await logtoClient.isAuthenticated()
if (!authenticated) {
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
return
}
window.location.href = createRepairPath
}
const handleEdit = (event: PublicEvent) => {
setSelectedEvent(event)
setIsEditOpen(true)
}
const handleCancel = async (event: PublicEvent) => {
try {
const logtoToken = await makeLogtoClient().getAccessToken()
await saturdayClient.DELETE("/client/events/{EventId}", {
params: {
path: {
EventId: event.eventId,
},
},
headers: {
Authorization: `Bearer ${logtoToken}`,
},
})
await fetchRecentEvents()
}
catch (error) {
console.error("Error cancelling event:", error)
}
}
const handleViewDetail = (event: PublicEvent) => {
window.location.href = `/repair/ticket-detail?eventId=${event.eventId}`
}
const handleEditSaved = () => {
fetchRecentEvents()
setIsEditOpen(false)
setSelectedEvent(null)
}
const handleViewAllHistory = () => {
window.location.href = "/repair/history"
}
if (loading) {
return (
<div className="container mx-auto pt-16 pb-20">
<div className="flex flex-col items-center justify-center">
<Spinner size="lg" />
</div>
</div>
)
}
return (
<div className="container mx-auto pt-16 pb-20">
{/* Main service section - always shown */}
<div className="flex flex-col items-center justify-center">
{/* eslint-disable-next-line @cspell/spellchecker */}
<img src={hayasaka.src} alt="" className="h-48 md:h-auto md:w-1/2 object-cover" />
<div className="mt-12 text-lg lg:text-2xl font-bold"></div>
<div className="mt-4 text-gray-500 text-center lg:text-lg">
<div>
<strong></strong><strong></strong><strong></strong>
</div>
<div>
</div>
</div>
<div className="mt-6">
<Button
className="bg-blue-500 text-white"
radius="full"
onPress={handleCreateTicketClick}
>
</Button>
</div>
</div>
{/* Recent events section - only for authenticated users */}
{userInfo && (
<div className="mt-16 section-content">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold"></h2>
<Button
color="primary"
variant="flat"
size="sm"
onPress={handleViewAllHistory}
>
</Button>
</div>
{recentEvents.length > 0
? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{recentEvents.map(event => (
<RepairHistoryCard
key={event.eventId}
event={event}
onEdit={handleEdit}
onCancel={handleCancel}
onViewDetail={handleViewDetail}
/>
))}
</div>
)
: (
<Card className="w-full">
<CardBody className="text-center py-8">
<p className="text-gray-500"></p>
</CardBody>
<CardFooter className="justify-center">
<Button color="primary" onPress={handleCreateTicketClick}>
</Button>
</CardFooter>
</Card>
)}
</div>
)}
{/* Login suggestion for unauthenticated users */}
{/* {!userInfo && (
<div className="mt-16 flex justify-center">
<Alert
className="items-center max-w-md"
endContent={(
<Button color="primary" size="sm" variant="flat" onPress={handleLogin}>
</Button>
)}
>
</Alert>
</div>
)} */}
{/* Edit Modal */}
{selectedEvent && (
<EditRepairModal
isOpen={isEditOpen}
onClose={() => setIsEditOpen(false)}
event={selectedEvent}
onSaved={handleEditSaved}
/>
)}
</div>
)
}

View file

@ -0,0 +1,208 @@
import { useState, useEffect } from "react"
import { Button, Spinner, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react"
import { makeLogtoClient } from "../../utils/auth"
import { saturdayClient } from "../../utils/client"
import EventDetail from "./EventDetail"
import EditRepairModal from "./EditRepairModal"
import type { UserInfoResponse } from "@logto/browser"
import type { components } from "../../types/saturday"
type PublicEvent = components["schemas"]["PublicEvent"]
export default function TicketDetail() {
const [userInfo, setUserInfo] = useState<UserInfoResponse | null>(null)
const [loading, setLoading] = useState(true)
const [eventId, setEventId] = useState<number | null>(null)
const [event, setEvent] = useState<PublicEvent | null>(null)
const { isOpen: isEditOpen, onOpen: onEditOpen, onClose: onEditClose } = useDisclosure()
const { isOpen: isCancelOpen, onOpen: onCancelOpen, onClose: onCancelClose } = useDisclosure()
useEffect(() => {
// Get eventId from URL parameters
const urlParams = new URLSearchParams(window.location.search)
const eventIdParam = urlParams.get("eventId")
if (eventIdParam) {
setEventId(parseInt(eventIdParam, 10))
}
checkAuthStatus()
}, [])
const checkAuthStatus = async () => {
try {
const logtoClient = makeLogtoClient()
const authenticated = await logtoClient.isAuthenticated()
if (authenticated) {
const claims = await logtoClient.getIdTokenClaims()
setUserInfo(claims)
}
else {
// Redirect to login if not authenticated
const redirectUrl = `/repair/ticket-detail${window.location.search}`
window.location.href = `/repair/login-hint?redirectUrl=${encodeURIComponent(redirectUrl)}`
}
}
catch (error) {
console.error("Error checking auth status:", error)
// Redirect to login on error
const redirectUrl = `/repair/ticket-detail${window.location.search}`
window.location.href = `/repair/login-hint?redirectUrl=${encodeURIComponent(redirectUrl)}`
}
finally {
setLoading(false)
}
}
const handleBackToHome = () => {
window.location.href = "/repair"
}
const handleEdit = () => {
if (event) {
onEditOpen()
}
}
const handleCancel = () => {
if (event) {
onCancelOpen()
}
}
const confirmCancel = async () => {
if (!event) return
try {
const logtoToken = await makeLogtoClient().getAccessToken()
await saturdayClient.DELETE("/client/events/{EventId}", {
params: {
path: {
EventId: event.eventId,
},
},
headers: {
Authorization: `Bearer ${logtoToken}`,
},
})
// Navigate back to history after cancellation
onCancelClose()
window.location.href = "/repair/history"
}
catch (err) {
console.error("Error cancelling event:", err)
}
}
const handleEditSaved = () => {
onEditClose()
// Refresh the event data
window.location.reload()
}
if (loading) {
return (
<div className="container mx-auto pt-16 pb-20">
<div className="flex flex-col items-center justify-center">
<Spinner size="lg" />
</div>
</div>
)
}
if (!userInfo || !eventId) {
return (
<div className="container mx-auto pt-16 pb-20">
<div className="flex flex-col items-center justify-center">
<p className="text-gray-500 mb-4">
{!userInfo ? "请先登录以查看维修详情" : "无效的维修单ID"}
</p>
<Button color="primary" onPress={handleBackToHome}>
</Button>
</div>
</div>
)
}
return (
<div className="container mx-auto pt-6 pb-20">
{/* Event detail content */}
<div className="section-content">
<EventDetail eventId={eventId}>
{(eventData: PublicEvent) => {
// Store the event data for actions
if (eventData && !event) {
setEvent(eventData)
}
const canModify = eventData.status !== "closed" && eventData.status !== "cancelled"
return (
<div className="mt-6 flex flex-col gap-4">
{/* Client Action Buttons */}
{canModify && (
<div className="flex gap-2">
<Button
color="primary"
variant="flat"
onPress={handleEdit}
>
</Button>
<Button
color="danger"
variant="flat"
onPress={handleCancel}
>
</Button>
</div>
)}
</div>
)
}}
</EventDetail>
{/* Edit Modal */}
{event && (
<EditRepairModal
isOpen={isEditOpen}
onClose={onEditClose}
event={event}
onSaved={handleEditSaved}
/>
)}
{/* Cancel Confirmation Modal */}
<Modal isOpen={isCancelOpen} onClose={onCancelClose} size="sm">
<ModalContent>
<ModalHeader></ModalHeader>
<ModalBody>
<p></p>
{event && (
<div className="mt-2 p-2 bg-gray-50 rounded">
<p className="text-sm font-medium">#{event.eventId}</p>
<p className="text-sm text-gray-600 line-clamp-2">{event.problem}</p>
</div>
)}
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onCancelClose}>
</Button>
<Button color="danger" onPress={confirmCancel}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
</div>
)
}

View file

@ -1,12 +1,8 @@
import { SITE_URL } from "../../consts"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { makeLogtoClient } from "../../utils/auth" import { makeLogtoClient } from "../../utils/auth"
import type { UserInfoResponse } from "@logto/browser" import type { UserInfoResponse } from "@logto/browser"
import { Alert, Form, Input, Button, Textarea } from "@heroui/react" import { Alert, Form, Input, Button, Textarea } from "@heroui/react"
import { saturdayClient } from "../../utils/client" import { saturdayClient } from "../../utils/client"
import type { components } from "../../types/saturday"
import QRCode from "qrcode"
import EventDetail from "./EventDetail"
type TicketFormData = { type TicketFormData = {
model?: string model?: string
@ -142,57 +138,9 @@ function TicketForm(props: {
</section> </section>
) )
} }
type PublicEvent = components["schemas"]["PublicEvent"]
function TicketFormCreated(props: {
event: PublicEvent
}) {
const [svg, setSvg] = useState<string>()
useEffect(() => {
const url = new URL(`/repair/ticket-detail`, SITE_URL)
url.searchParams.append("eventId", props.event.eventId.toString())
QRCode.toString(url.toString()).then((res) => {
setSvg(res)
})
})
return (
<section className="box-border w-full mt-8">
<div className="section-content mb-4">
<Alert hideIcon color="success" variant="faded">
<div className="flex items-center gap-8">
<div className="h-28 lg:h-40 aspect-square">
<div className="h-full" dangerouslySetInnerHTML={{ __html: svg }}></div>
</div>
<div>
<div className="flex items-center gap-2">
<div className="text-brand text-nowrap text-lg lg:text-2xl font-bold">
</div>
</div>
<div className="mt-1 text-gray-600 lg:text-lg">
<div>
</div>
<div>
</div>
</div>
</div>
</div>
</Alert>
</div>
<div className="section-content mb-4">
<EventDetail eventId={props.event?.eventId}>
{() => <></>}
</EventDetail>
</div>
</section>
)
}
export default function App() { export default function App() {
const [userInfo, setUserInfo] = useState<UserInfoResponse>() const [userInfo, setUserInfo] = useState<UserInfoResponse>()
const [event, setEvent] = useState<PublicEvent | undefined>()
useEffect(() => { useEffect(() => {
const check = async () => { const check = async () => {
@ -206,31 +154,6 @@ export default function App() {
setUserInfo(res) setUserInfo(res)
} }
check() check()
// Check for eventId in URL query parameters
const urlParams = new URLSearchParams(window.location.search)
const eventId = urlParams.get("eventId")
if (eventId) {
// Fetch event data from the eventId
const fetchEvent = async () => {
try {
const { data } = await saturdayClient.GET("/events/{EventId}", {
params: {
path: {
EventId: parseInt(eventId, 10),
},
},
})
if (data) {
setEvent(data as PublicEvent)
}
}
catch (error) {
console.log("Error fetching event:", error)
}
}
fetchEvent()
}
}, []) }, [])
const onSubmit = async (formData: TicketFormData) => { const onSubmit = async (formData: TicketFormData) => {
@ -247,13 +170,9 @@ export default function App() {
qq: formData.qq, qq: formData.qq,
}, },
}) })
setEvent(data as unknown as PublicEvent)
// Update URL with eventId to persist the ticket status // Update URL with eventId to persist the ticket status
if (data?.eventId) { if (data?.eventId) {
const currentUrl = new URL(window.location.href) window.location.href = `/repair/ticket-detail?eventId=${data.eventId}`
currentUrl.searchParams.set("eventId", data.eventId.toString())
window.history.pushState({}, "", currentUrl.toString())
} }
} }
catch (error) { catch (error) {
@ -262,12 +181,6 @@ export default function App() {
} }
return ( return (
<> <TicketForm userInfo={userInfo} onSubmit={onSubmit}></TicketForm>
{
event?.eventId
? <TicketFormCreated event={event}></TicketFormCreated>
: <TicketForm userInfo={userInfo} onSubmit={onSubmit}></TicketForm>
}
</>
) )
} }

View file

@ -0,0 +1,109 @@
import { useState, useEffect } from "react"
import { Alert, Button, Spinner } from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import { makeLogtoClient } from "../../utils/auth"
import RepairHistoryCard from "./RepairHistoryCard"
import type { components } from "../../types/saturday"
type PublicEvent = components["schemas"]["PublicEvent"]
interface UserRepairHistoryProps {
onCreateNew: () => void
}
export default function UserRepairHistory({ onCreateNew }: UserRepairHistoryProps) {
const [events, setEvents] = useState<PublicEvent[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string>("")
const fetchUserEvents = async () => {
try {
setLoading(true)
const logtoToken = await makeLogtoClient().getAccessToken()
const { data, error: apiError } = await saturdayClient.GET("/client/events", {
headers: {
Authorization: `Bearer ${logtoToken}`,
},
params: {
query: {
limit: 50,
offset: 0,
},
},
})
if (apiError || !data) {
throw new Error("Failed to fetch repair history")
}
setEvents(data || [])
}
catch (err) {
console.error("Error fetching user events:", err)
setError("获取维修记录失败")
}
finally {
setLoading(false)
}
}
useEffect(() => {
fetchUserEvents()
}, [])
const handleViewDetail = (event: PublicEvent) => {
window.location.href = `/repair/ticket-detail?eventId=${event.eventId}`
}
if (loading) {
return (
<div className="flex justify-center items-center py-8">
<Spinner size="lg" />
</div>
)
}
if (error) {
return (
<Alert color="danger" className="mb-4">
{error}
<Button size="sm" color="danger" variant="flat" onPress={fetchUserEvents}>
</Button>
</Alert>
)
}
if (events.length === 0) {
return (
<div className="text-center py-8">
<p className="text-gray-500 mb-4"></p>
<Button color="primary" onPress={onCreateNew}>
</Button>
</div>
)
}
return (
<div className="w-full">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold"></h2>
<Button color="primary" onPress={onCreateNew}>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{events.map(event => (
<RepairHistoryCard
key={event.eventId}
event={event}
onViewDetail={handleViewDetail}
/>
))}
</div>
</div>
)
}

View file

@ -0,0 +1,15 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
import RepairHeader from "../../components/header/RepairHeader.astro"
import RepairHistoryPage from "./RepairHistoryPage.tsx"
---
<BaseLayout primaryTitle="维修历史">
<section class="box-border">
<RepairHeader></RepairHeader>
<!-- Repair history page content -->
<RepairHistoryPage client:only="react"></RepairHistoryPage>
</section>
</BaseLayout>

View file

@ -1,57 +1,18 @@
--- ---
import BaseLayout from "../../layouts/BaseLayout.astro" import BaseLayout from "../../layouts/BaseLayout.astro"
import hayasaka from "../_assets/hayasaka.jpg"
import repairDayOnSite from "../_assets/repair_day_on_site.jpeg" import repairDayOnSite from "../_assets/repair_day_on_site.jpeg"
import { Button } from "@heroui/react"
import RepairHeader from "../../components/header/RepairHeader.astro" import RepairHeader from "../../components/header/RepairHeader.astro"
import RepairLandingSection from "./RepairLandingSection.tsx"
--- ---
<script>
// @ts-check
import { makeLogtoClient } from "../../utils/auth"
const button = document.getElementById("create-ticket-button")
button.addEventListener("click", async () => {
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
}
window.location.href = createRepairPath
})
</script>
<BaseLayout> <BaseLayout>
<section class="box-border"> <section class="box-border">
<RepairHeader></RepairHeader> <RepairHeader></RepairHeader>
<div class="container mx-auto pt-16 pb-20">
<div class="flex flex-col items-center justify-center"> <!-- Dynamic content based on authentication status -->
<img src={hayasaka.src} alt="" class="h-48 md:h-auto md:w-1/2 object-cover" /> <RepairLandingSection client:only="react"></RepairLandingSection>
<div class="mt-12 text-lg lg:text-2xl font-bold">我们提供免费的电脑维修服务</div>
<div class="mt-4 text-gray-500 text-center lg:text-lg">
<div>
从<strong>清理磁盘</strong>到<strong>加装硬件</strong>再到<strong>环境配置</strong>
</div>
<div>
我们都帮你搞定。
</div>
</div>
<div class="mt-6">
<Button
id="create-ticket-button"
className="bg-blue-500 text-white"
radius="full"
>进行预约
</Button>
</div>
</div>
</div>
<div class="w-full bg-white pb-24 flex flex-col gap-12"> <div class="w-full bg-white pb-24 flex flex-col gap-12">
<div class="component"> <div class="component">
<div class="relative h-[521px] sm:h-[396px] md:h-[561px] mt-4 w-full my-0 xs:rounded-2xl overflow-hidden"> <div class="relative h-[521px] sm:h-[396px] md:h-[561px] mt-4 w-full my-0 xs:rounded-2xl overflow-hidden">
@ -75,12 +36,3 @@ import RepairHeader from "../../components/header/RepairHeader.astro"
</div> </div>
</section> </section>
</BaseLayout> </BaseLayout>
<script>
// @ts-check
import { makeLogtoClient } from "../../utils/auth"
makeLogtoClient().getIdTokenClaims().then((res) => {
console.log(res)
})
</script>

View file

@ -1,12 +1,10 @@
--- ---
import BaseLayout from "../../layouts/BaseLayout.astro" import BaseLayout from "../../layouts/BaseLayout.astro"
import RepairHeader from "../../components/header/RepairHeader.astro" import RepairHeader from "../../components/header/RepairHeader.astro"
import EventDetail from "./EventDetail" import TicketDetailComponent from "./TicketDetail.tsx"
--- ---
<BaseLayout primaryTitle="Create Ticket"> <BaseLayout primaryTitle="维修详情">
<RepairHeader></RepairHeader> <RepairHeader></RepairHeader>
<div class="section-content"> <TicketDetailComponent client:only="react"></TicketDetailComponent>
<EventDetail client:only="react"></EventDetail>
</div>
</BaseLayout> </BaseLayout>