Merge pull request #79 from wen-templari/repair

feat: add repair related pages
This commit is contained in:
clas 2025-05-19 12:53:09 +08:00 committed by GitHub
commit 53d6723dfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 5993 additions and 5629 deletions

View file

@ -35,6 +35,10 @@ export default defineConfig({
changeOrigin: true, changeOrigin: true,
rewrite: path => path.replace(/^\/active/, ""), rewrite: path => path.replace(/^\/active/, ""),
}, },
"/saturday": {
target: "http://localhost:4000",
rewrite: path => path.replace(/^\/saturday/, ""),
},
} }
} }
} }

View file

@ -14,53 +14,54 @@
"active": "openapi-ts -f openapi-ts.active.config.ts" "active": "openapi-ts -f openapi-ts.active.config.ts"
}, },
"dependencies": { "dependencies": {
"@astrojs/react": "^3.6.2", "@astrojs/react": "^3.6.3",
"@astrojs/rss": "^4.0.7", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^5.1.1", "@astrojs/tailwind": "^5.1.5",
"@astrojs/vue": "^4.5.1", "@astrojs/vue": "^4.5.3",
"@fullcalendar/core": "^6.1.15", "@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.15", "@fullcalendar/daygrid": "^6.1.17",
"@fullcalendar/icalendar": "^6.1.15", "@fullcalendar/icalendar": "^6.1.17",
"@fullcalendar/react": "^6.1.15", "@fullcalendar/react": "^6.1.17",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroui/react": "2.7.6",
"@logto/browser": "^2.2.18", "@logto/browser": "^2.2.18",
"@heroui/react": "2.6.14", "@stylistic/eslint-plugin": "^2.13.0",
"@stylistic/eslint-plugin": "^2.8.0",
"astro": "^4.16.18", "astro": "^4.16.18",
"framer-motion": "^11.9.0", "dayjs": "^1.11.13",
"framer-motion": "^11.18.2",
"ical.js": "^1.5.0", "ical.js": "^1.5.0",
"md5": "^2.3.0", "md5": "^2.3.0",
"openapi-fetch": "^0.12.2", "openapi-fetch": "^0.12.5",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"rehype": "^13.0.2", "rehype": "^13.0.2",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.17",
"unist-util-visit": "^5.0.0", "unist-util-visit": "^5.0.0",
"uuid": "10.0.0", "uuid": "10.0.0",
"vue": "^3.5.10" "vue": "^3.5.13"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/markdown-remark": "^5.2.0", "@astrojs/markdown-remark": "^5.3.0",
"@cspell/eslint-plugin": "^8.14.4", "@cspell/eslint-plugin": "^8.19.2",
"@eslint/js": "^9.11.1", "@eslint/js": "^9.25.1",
"@hey-api/openapi-ts": "^0.53.5", "@hey-api/openapi-ts": "^0.53.12",
"@types/eslint__js": "^8.42.3", "@types/eslint__js": "^8.42.3",
"@types/md5": "^2.3.5", "@types/md5": "^2.3.5",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/react": "^18.3.10", "@types/react": "^18.3.20",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.6",
"@types/uuid": "10.0.0", "@types/uuid": "10.0.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.6.3", "eslint-import-resolver-typescript": "^3.10.1",
"eslint-plugin-astro": "^1.2.4", "eslint-plugin-astro": "^1.3.1",
"eslint-plugin-import": "^2.30.0", "eslint-plugin-import": "^2.31.0",
"globals": "^15.9.0", "globals": "^15.15.0",
"husky": "^9.1.6", "husky": "^9.1.7",
"lint-staged": "^15.2.10", "lint-staged": "^15.5.1",
"openapi-typescript": "^7.4.1", "openapi-typescript": "^7.6.1",
"typescript": "^5.6.2", "typescript": "^5.8.3",
"typescript-eslint": "8.7.0" "typescript-eslint": "8.7.0"
}, },
"lint-staged": { "lint-staged": {

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ import HeaderNavigation from "./HeaderNavigation"
<nav class="nav"> <nav class="nav">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<div class="max-w-[1005px] w-full"> <div class="w-full flex items-center">
<HeaderNavigation client:load /> <HeaderNavigation client:load />
</div> </div>
</div> </div>

View file

@ -20,14 +20,14 @@ export default function App() {
name: "关于我们", name: "关于我们",
}, },
{ {
link: "/join-us", link: "/repair",
name: "加入我们", name: "维修",
}, },
] ]
return ( return (
<Navbar onMenuOpenChange={setIsMenuOpen} height="48px"> <Navbar onMenuOpenChange={setIsMenuOpen} height="48px" className="">
<NavbarContent className="flex justify-between items-center"> <NavbarContent className="flex justify-between items-center px-0 md:px-[22px]">
<NavbarBrand className="flex gap-4"> <NavbarBrand className="flex gap-4">
<img <img
src="https://oss.nbtca.space/CA-logo.svg" src="https://oss.nbtca.space/CA-logo.svg"

View file

@ -7,7 +7,11 @@ import type LogtoClient from "@logto/browser"
const logtoClient = ref<LogtoClient>() const logtoClient = ref<LogtoClient>()
const onSignIn = async () => { const onSignIn = async () => {
logtoClient.value?.signIn(import.meta.env.PUBLIC_LOGTO_CALLBACK_URL) console.log(window.location.pathname)
logtoClient.value?.signIn({
redirectUri: import.meta.env.PUBLIC_LOGTO_CALLBACK_URL,
postRedirectUri: window.location.pathname,
})
} }
const onSignOut = async () => { const onSignOut = async () => {
logtoClient.value?.signOut(import.meta.env.PUBLIC_LOGTO_REDIRECT_URL) logtoClient.value?.signOut(import.meta.env.PUBLIC_LOGTO_REDIRECT_URL)
@ -34,9 +38,9 @@ onMounted(() => {
}) })
</script> </script>
<template> <template>
<div class="flex items-center justify-center w-12"> <div class="flex items-center justify-center">
<div @click="onSignIn" v-if="isAuthenticated === false" class=""> <div @click="onSignIn" v-if="isAuthenticated === false" class="">
<a class="nav-item-content px-2 hover:text-[#2997ff] text-nowrap cursor-pointer">登入</a> <a class="nav-item-content hover:text-[#2997ff] text-nowrap cursor-pointer">登入</a>
</div> </div>
<div class="flex items-center" v-if="isAuthenticated"> <div class="flex items-center" v-if="isAuthenticated">
<Menu as="div" class="relative inline-block text-left"> <Menu as="div" class="relative inline-block text-left">

View file

@ -0,0 +1,17 @@
---
import NavigationUser from "./NavigationUser.vue"
---
<script>
const button = document.getElementById("repair-header")
button.addEventListener("click", () => {
window.location.href = "/repair"
})
</script>
<div class="box-border border-b sticky top-0 bg-white/80 backdrop-blur z-20 h-12">
<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>
<NavigationUser client:load />
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 KiB

View file

@ -123,14 +123,14 @@ import hayasaka from "../_assets/hayasaka.jpg"
<div class="h-full bg-white rounded-2xl overflow-hidden flex flex-col items-center justify-center p-6 xl:p-8"> <div class="h-full bg-white rounded-2xl overflow-hidden flex flex-col items-center justify-center p-6 xl:p-8">
<div class="font-bold text-center"> <div class="font-bold text-center">
<div> <div>
扫一扫, 遇到问题?
</div> </div>
<div> <div>
获取免费维修。 线上预约维修。
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<a href="/repair/mini-program" class="text-base ">查看报修小程序{" >"}</a> <a href="/repair" class="text-base ">预约维修{" >"}</a>
</div> </div>
</div> </div>
<div class="h-full bg-white rounded-2xl overflow-hidden flex flex-col items-center justify-center p-6 xl:p-8"> <div class="h-full bg-white rounded-2xl overflow-hidden flex flex-col items-center justify-center p-6 xl:p-8">

View file

@ -2,22 +2,29 @@
<script> <script>
import { makeLogtoClient } from "../utils/auth" import { makeLogtoClient } from "../utils/auth"
import LogtoClient from "@logto/browser"
const callbackHandler = async (logtoClient: LogtoClient) => { const callbackHandler = async (logtoClient) => {
console.log("callbackHandler")
try { try {
await logtoClient.handleSignInCallback(window.location.href) await logtoClient.handleSignInCallback(window.location.href)
if (!logtoClient.isAuthenticated) { if (!logtoClient.isAuthenticated) {
console.log("User not authenticated")
window.location.assign("/") window.location.assign("/")
return return
} }
// Handle successful sign-in
window.location.assign("/")
} }
catch (error) { catch (error) {
console.log(error)
window.location.assign("/") window.location.assign("/")
} }
} }
const logtoClient = makeLogtoClient()
callbackHandler(logtoClient) console.log("Callback page loaded")
try {
const logtoClient = makeLogtoClient()
callbackHandler(logtoClient)
}
catch (error) {
console.log(error)
}
</script> </script>

View file

@ -0,0 +1,164 @@
import { useEffect, useState } from "react"
import type { components } from "../../types/saturday"
import { saturdayClient } from "../../utils/client"
import { Textarea, Input, Chip } from "@heroui/react"
import type { PublicMember } from "../../store/member"
import dayjs from "dayjs"
import { EventStatus, UserEventAction } from "../../types/event"
type PublicEvent = components["schemas"]["PublicEvent"]
type EventLog = components["schemas"]["EventLog"]
function EventLogItem(props: {
eventLog: EventLog
actor?: PublicMember
}) {
return (
<div className="py-1 flex items-center">
{/* <div className="mr-4 h-10 bg-red-400 flex flex-col items-center gap-2">
</div> */}
<div>
<div className="flex items-center">
<div className="mr-4">
{
UserEventAction.find(v => v.action === props.eventLog.action)?.text || props.eventLog.action
}
</div>
<div className="flex items-center">
{
props.actor?.avatar
? <img src={props.actor?.avatar} alt="actor avatar" className="w-6 aspect-square rounded-full" />
: <></>
}
<span className="text-gray-600 ml-2">
{
props.actor ? props.actor.alias : ""
}
</span>
</div>
</div>
<div className="flex gap-2 items-center mt-1 text-gray-600">
{dayjs(props.eventLog.gmtCreate).format("YYYY-MM-DD HH:mm")}
</div>
</div>
</div>
)
}
function EventStatusChip(props: {
status: string
}) {
switch (props.status) {
case EventStatus.open:
return <Chip></Chip>
case EventStatus.accepted:
return <Chip></Chip>
case EventStatus.committed:
return <Chip color="primary"></Chip>
case EventStatus.closed:
return <Chip color="success"></Chip>
case EventStatus.cancelled:
return <Chip></Chip>
}
}
const filterEventLog = (event: PublicEvent) => {
const eventLogs = event.logs
const filteredLogs: (EventLog & { actor?: PublicMember })[] = []
// find the first log that action is "create"
const createLog = eventLogs.find(log => log.action === "create")
filteredLogs.push(createLog)
// find the first log that action is "cancel"
const cancelLog = eventLogs.find(log => log.action === "cancel")
if (cancelLog) {
filteredLogs.push(cancelLog)
return filteredLogs
}
// find the last log that action is "accept"
const acceptLog = eventLogs.findLast(log => log.action === "accept")
if (acceptLog) {
filteredLogs.push({
...acceptLog,
actor: event.member,
})
}
// find the last log that action is "close"
const closeLog = eventLogs.findLast(log => log.action === "close")
if (closeLog) {
filteredLogs.push({
...closeLog,
actor: event.closedBy,
})
}
return filteredLogs
}
export default function EventDetail(props: {
eventId?: number
}) {
const [event, setEvent] = useState<PublicEvent | undefined>()
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
? (
<section className="box-border mb-24">
<div className="section-content mt-8">
<h2 className="text-2xl font-bold"></h2>
<div className="flex gap-2 items-center">
<span>
#{event.eventId}
</span>
<EventStatusChip status={event.status}></EventStatusChip>
</div>
</div>
<div className="section-content my-8 flex flex-col gap-4">
<Textarea
label="问题描述"
readOnly
name="description"
value={event.problem || ""}
/>
<Input
label="型号"
type="text"
value={event.model || ""}
readOnly
>
</Input>
<div className="bg-gray-100 rounded-xl text-sm px-3 py-2 mt-2 ">
<div className="text-xs font-semibold text-gray-600 mb-1">
</div>
{
filterEventLog(event).map((v, index) => {
return (
<EventLogItem key={index} actor={v.actor} eventLog={v} />
)
})
}
</div>
</div>
</section>
)
: <div></div>
)
}

View file

@ -0,0 +1,238 @@
import { SITE_URL } from "../../consts"
import { useEffect, useState } from "react"
import { makeLogtoClient } from "../../utils/auth"
import type { UserInfoResponse } from "@logto/browser"
import { Alert, Form, Input, Button, Textarea } from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import type { components } from "../../types/saturday"
import QRCode from "qrcode"
import EventDetail from "./EventDetail"
type TicketFormData = {
model?: string
phone?: string
qq?: string
description?: string
}
function LoginHintAlert(props: {
onLogin: () => void
}) {
return (
<div className="section-content my-8">
<div className="flex items-center justify-center w-full">
<Alert
className="items-center"
endContent={(
<Button color="primary" size="sm" variant="flat" onPress={props.onLogin}>
</Button>
)}
>
</Alert>
</div>
</div>
)
}
function TicketForm(props: {
userInfo?: UserInfoResponse
onSubmit: (form: TicketFormData) => Promise<void>
}) {
const [loading, setLoading] = useState<boolean>()
const [formData, setFormData] = useState<TicketFormData>({})
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setLoading(true)
await props.onSubmit(formData)
setLoading(false)
}
const onLogin = async () => {
makeLogtoClient().signIn({
redirectUri: import.meta.env.PUBLIC_LOGTO_CALLBACK_URL,
postRedirectUri: window.location.pathname,
})
}
return (
<section className="box-border mb-24">
{
props.userInfo?.sub
? <></>
: <LoginHintAlert onLogin={onLogin}></LoginHintAlert>
}
<div className="section-content my-8">
<h2 className="text-2xl font-bold"></h2>
<div>
</div>
</div>
<div className="section-content w-full">
<Form
className="w-full flex flex-col gap-4"
onSubmit={onSubmit}
>
<div className="text-sm font-bold mx-1">
</div>
<Textarea
label="问题描述"
placeholder="告诉我们你遇到的问题"
errorMessage="问题描述不能为空"
required
name="description"
value={formData.description || ""}
onChange={(e) => {
setFormData({ ...formData, description: e.target.value })
}}
isRequired
/>
<div className="w-full">
<Input
label="型号"
type="text"
placeholder="你的设备型号"
value={formData.model || ""}
onChange={(e) => {
setFormData({ ...formData, model: e.target.value })
}}
description="填写设备型号,帮助我们更快的定位问题"
/>
</div>
<div className="text-sm font-bold mx-1 mt-2">
</div>
<Input
label="邮箱"
placeholder="example@nbtca.space"
isRequired
type="email"
value={props.userInfo?.email || ""}
readOnly
description="我们会向此邮箱发送维修相关的通知"
/>
<Input
label="电话号码"
placeholder="必填"
errorMessage="电话号码不能为空"
isRequired
type="phone"
name="phone"
value={formData.phone || ""}
maxLength={11}
onChange={(e) => {
setFormData({ ...formData, phone: e.target.value })
}}
/>
<Input
label="QQ"
placeholder="你的QQ号"
value={formData.qq || ""}
onChange={(e) => {
setFormData({ ...formData, qq: e.target.value })
}}
/>
<Button type="submit" color="primary" className="my-4 w-full" isLoading={loading}>
</Button>
</Form>
</div>
</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">
<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>
<EventDetail eventId={props.event?.eventId}></EventDetail>
</section>
)
}
export default function App() {
const [userInfo, setUserInfo] = useState<UserInfoResponse>()
const [event, setEvent] = useState<PublicEvent | undefined>()
useEffect(() => {
const check = async () => {
const createRepairPath = "/repair/create-ticket"
const authenticated = await makeLogtoClient().isAuthenticated()
if (!authenticated) {
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
return
}
const res = await makeLogtoClient().getIdTokenClaims()
setUserInfo(res)
}
check()
}, [])
const onSubmit = async (formData: TicketFormData) => {
const logtoToken = await makeLogtoClient().getAccessToken()
try {
const res = await saturdayClient.POST("/client/event", {
headers: {
Authorization: `Bearer ${logtoToken}`,
},
body: {
Problem: formData.description,
model: formData.model,
phone: formData.phone,
qq: formData.qq,
},
})
setEvent(res.data as unknown as PublicEvent)
}
catch (error) {
console.log(error)
}
}
return (
<>
{
event?.eventId
? <TicketFormCreated event={event}></TicketFormCreated>
: <TicketForm userInfo={userInfo} onSubmit={onSubmit}></TicketForm>
}
</>
)
}

View file

@ -0,0 +1,12 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
import RepairHeader from "../../components/header/RepairHeader.astro"
import TicketForm from "./TicketForm"
---
<BaseLayout primaryTitle="Create Ticket">
<RepairHeader></RepairHeader>
<div>
<TicketForm client:only="react"></TicketForm>
</div>
</BaseLayout>

View file

@ -0,0 +1,83 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
import hayasaka from "../_assets/hayasaka.jpg"
import repairDayOnSite from "../_assets/repair_day_on_site.jpeg"
import { Button } from "@heroui/react"
import RepairHeader from "../../components/header/RepairHeader.astro"
---
<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()
if (!authenticated) {
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
return
}
window.location.href = createRepairPath
})
</script>
<BaseLayout>
<section class="box-border">
<RepairHeader></RepairHeader>
<div class="container mx-auto pt-16 pb-20">
<div class="flex flex-col items-center justify-center">
<img src={hayasaka.src} alt="" class="h-48 md:h-auto md:w-1/2 object-cover" />
<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="component">
<div class="relative h-[521px] sm:h-[396px] md:h-[561px] mt-4 w-full my-0 xs:rounded-2xl overflow-hidden">
<img src={repairDayOnSite.src} alt="" class="w-full h-full object-cover object-bottom-right" />
<div class="absolute bottom-0 left-0 text-white font-bold p-4 text-xl flex items-center gap-2">
理工维修日
<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.9998 6.04438C22.9698 4.81543 22.5749 3.62319 21.8654 2.61935C21.1558 1.6155 20.1637 0.845446 19.0152 0.407155C17.8667 -0.0311353 16.6138 -0.117845 15.4159 0.15806C14.218 0.433964 13.1292 1.06001 12.2881 1.95654L9.80055 4.70897C9.09069 5.49362 8.84036 5.08415 8.42884 4.69594C3.47217 0.0257864 -2.93372 6.51078 1.47633 11.4134L8.22583 18.9197L11.2806 15.5184C10.3522 15.6164 9.41428 15.4701 8.55984 15.0939C7.374 14.574 6.57978 13.6275 6.16826 12.9944C5.67239 12.2317 5.47829 11.3387 5.33084 10.3394C5.21836 9.57872 4.91932 9.05882 4.54004 8.65621C4.06036 8.14157 3.40641 7.82375 2.70538 7.76457C2.79582 7.24026 3.06807 6.76458 3.47431 6.42103C3.88056 6.07748 4.39484 5.88803 4.92687 5.88594C5.23782 5.88279 5.54591 5.94558 5.83082 6.07019C6.11573 6.1948 6.371 6.37839 6.57978 6.60886C7.11406 7.22135 7.67029 7.67677 8.32391 7.95593C9.93087 8.64181 11.1037 6.92711 11.6002 6.3784L14.8875 2.76107C15.5473 2.03541 16.607 2.37904 16.9719 2.53405L13.1071 6.79679C13.0827 6.82374 13.0644 6.85558 13.0534 6.89019C13.0424 6.92479 13.0389 6.96135 13.0432 6.99741C13.0475 7.03347 13.0595 7.06819 13.0783 7.09924C13.0971 7.1303 13.1223 7.15697 13.1523 7.17745C13.203 7.21186 13.2643 7.22692 13.3251 7.21986C13.3859 7.21281 13.4422 7.18413 13.4836 7.13904L16.9328 3.35642C17.4396 2.80017 18.1556 2.80085 18.9986 3.20552L14.4719 8.22205C14.4287 8.26989 14.4063 8.33293 14.4096 8.39731C14.4128 8.46169 14.4416 8.52213 14.4894 8.56533C14.5372 8.60853 14.6003 8.63096 14.6647 8.62768C14.729 8.6244 14.7895 8.59568 14.8327 8.54784L18.4766 4.51007C18.9121 4.02584 19.6391 4.17742 20.4272 4.54985L15.9342 9.53414C15.8942 9.57834 15.8735 9.63659 15.8765 9.69608C15.8795 9.75558 15.9061 9.81143 15.9503 9.85136C15.9945 9.89128 16.0527 9.91202 16.1122 9.90899C16.1717 9.90597 16.2276 9.87944 16.2675 9.83524L18.882 6.94494C19.4197 6.34891 20.1995 6.53341 20.8476 6.87292L9.31222 19.8457L9.18603 19.9876L10.9953 22L21.2831 10.5582C22.3949 9.4045 23.043 7.76662 22.9978 6.04438" fill="white" />
</svg>
</div>
</div>
<div class="mt-4 font-semibold px-4 xs:px-2 text-base xs:text-lg text-gray-400">
<div>
在接受预约之外,我们每月举办 <span class="text-black">理工维修日</span>,定时定点提供维修。
</div>
<div>
在<a href="/calendar" class="mx-0.5">协会公共日历</a>上查看最近的维修日
</div>
</div>
</div>
</div>
</section>
</BaseLayout>
<script>
// @ts-check
import { makeLogtoClient } from "../../utils/auth"
makeLogtoClient().getIdTokenClaims().then((res) => {
console.log(res)
})
</script>

View file

@ -0,0 +1,47 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
import RepairHeader from "../../components/header/RepairHeader.astro"
import { Button } from "@heroui/react"
---
<script>
import { makeLogtoClient } from "../../utils/auth"
const button = document.getElementById("login-button")
button.addEventListener("click", async () => {
const logtoClient = makeLogtoClient()
const urlParams = new URLSearchParams(window.location.search)
const redirectUrl = urlParams.get("redirectUrl") || "/"
const isAuthenticated = await logtoClient.isAuthenticated()
if (isAuthenticated) {
window.location.href = redirectUrl
}
logtoClient.signIn({
redirectUri: import.meta.env.PUBLIC_LOGTO_CALLBACK_URL,
postRedirectUri: redirectUrl,
})
})
</script>
<BaseLayout primaryTitle="Create Ticket">
<RepairHeader></RepairHeader>
<section class="min-h-[70vh]">
<div class="section-content my-16 flex flex-col gap-8">
<div>
<div>
<div class="text-xl font-bold">登入账号后继续操作</div>
<div class="text-sm text-gray-700">
前往 auth.app.nbtca.space
</div>
</div>
<div class="mt-4">
<div class="text-gray-700">为了验证身份以及记录维修信息,请在登入帐号之后继续操作。</div>
<div class="mt-1 text-gray-700">你也可以点击下方登入按钮后注册账户。</div>
</div>
</div>
<div class="mt-4">
<Button id="login-button" color="primary">登入</Button>
</div>
</div>
</section>
</BaseLayout>

View file

@ -1,23 +0,0 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
import RepairMiniProgramQRCode from "../_assets/repair_mini_program_qr_code.jpg"
---
<BaseLayout primaryTitle="关于">
<div class="min-h-screen bg-white flex flex-col items-center">
<div class="py-32 flex flex-col items-center">
<div class="text-3xl lg:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-pink-500 to-violet-500">
NBT 电脑维修小程序
</div>
<div class="text-center text-xl lg:text-2xl text-[#004f8e] font-bold mt-8 mb-10">
<div>
微信扫码
</div>
<div>
获取免费维修
</div>
</div>
<img src={RepairMiniProgramQRCode.src} alt="" class="w-64 aspect-square" />
</div>
</div>
</BaseLayout>

View file

@ -0,0 +1,12 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
import RepairHeader from "../../components/header/RepairHeader.astro"
import EventDetail from "./EventDetail"
---
<BaseLayout primaryTitle="Create Ticket">
<RepairHeader></RepairHeader>
<div>
<EventDetail client:only="react"></EventDetail>
</div>
</BaseLayout>

78
src/types/event.ts Normal file
View file

@ -0,0 +1,78 @@
export interface Status {
status: string
text: string
icon: string
}
export interface Action {
action: string
text: string
icon: string
}
export enum EventStatus {
open = "open",
accepted = "accepted",
committed = "committed",
closed = "closed",
cancelled = "cancelled",
}
export const UserEventStatus: Status[] = [
{
status: EventStatus.open,
text: "待处理",
icon: "status_initial.svg",
},
{
status: EventStatus.accepted,
text: "维修中",
icon: "status_ongoing.svg",
},
{
status: EventStatus.committed,
text: "维修中",
icon: "status_ongoing.svg",
},
{
status: EventStatus.closed,
text: "已完成",
icon: "status_complete.svg",
},
{
status: EventStatus.cancelled,
text: "已取消",
icon: "status_cancelled.svg",
},
]
export enum EventAction {
create = "create",
accept = "accept",
commit = "commit",
close = "close",
cancel = "cancel",
}
export const UserEventAction: Action[] = [
{
action: EventAction.create,
text: "事件创建",
icon: "event_create.svg",
},
{
action: EventAction.accept,
text: "维修开始",
icon: "status_ongoing.svg",
},
{
action: EventAction.close,
text: "维修完成",
icon: "status_complete.svg",
},
{
action: EventAction.cancel,
text: "事件取消",
icon: "status_cancelled.svg",
},
]

View file

@ -1,7 +1,14 @@
import LogtoClient from "@logto/browser" import LogtoClient from "@logto/browser"
export const makeLogtoClient = () => let logtoClient: LogtoClient | undefined = undefined
new LogtoClient({
export const makeLogtoClient = () => {
if (logtoClient === undefined) {
logtoClient = new LogtoClient({
endpoint: import.meta.env.PUBLIC_LOGTO_ENDPOINT, endpoint: import.meta.env.PUBLIC_LOGTO_ENDPOINT,
appId: import.meta.env.PUBLIC_LOGTO_APP_ID, appId: import.meta.env.PUBLIC_LOGTO_APP_ID,
scopes: ["email", "custom_data", "roles"],
}) })
}
return logtoClient
}

View file

@ -1,15 +1,13 @@
import createClient from "openapi-fetch" import createClient from "openapi-fetch"
import type { paths as saturdayPaths } from "../types/saturday" import type { paths as saturdayPaths } from "../types/saturday"
// import type { paths as activePaths } from "../types/active"
import { ApiClient } from "./active" import { ApiClient } from "./active"
export const saturdayClient = createClient<saturdayPaths>({ export const saturdayClient = createClient<saturdayPaths>({
baseUrl: "https://api.nbtca.space/v2/", baseUrl: import.meta.env.PROD ? "https://api.nbtca.space/v2/" : "/saturday",
}) })
// export const activeClient = createClient<activePaths>({
// baseUrl: "https://active.nbtca.space/",
// })
export const activeClient = new ApiClient({ export const activeClient = new ApiClient({
BASE: "https://active.nbtca.space", BASE: "https://active.nbtca.space",
// BASE: "/active",
}) })
export * from "./active/types.gen" export * from "./active/types.gen"

View file

@ -1,11 +1,14 @@
import { heroui } from "@heroui/react" import { heroui } from "@heroui/react"
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: [ content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
screens: {
xs: "416px",
sm: "734px",
md: "1068px",
lg: "1441px",
},
extend: { extend: {
colors: { colors: {
brand: "#004b86", brand: "#004b86",