mirror of
https://github.com/m1ngsama/FUJI.git
synced 2025-12-24 10:51:27 +00:00
Merge pull request #79 from wen-templari/repair
feat: add repair related pages
This commit is contained in:
commit
53d6723dfb
22 changed files with 5993 additions and 5629 deletions
|
|
@ -35,6 +35,10 @@ export default defineConfig({
|
|||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/active/, ""),
|
||||
},
|
||||
"/saturday": {
|
||||
target: "http://localhost:4000",
|
||||
rewrite: path => path.replace(/^\/saturday/, ""),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
57
package.json
57
package.json
|
|
@ -14,53 +14,54 @@
|
|||
"active": "openapi-ts -f openapi-ts.active.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/react": "^3.6.2",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@astrojs/tailwind": "^5.1.1",
|
||||
"@astrojs/vue": "^4.5.1",
|
||||
"@fullcalendar/core": "^6.1.15",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
"@fullcalendar/icalendar": "^6.1.15",
|
||||
"@fullcalendar/react": "^6.1.15",
|
||||
"@astrojs/react": "^3.6.3",
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/tailwind": "^5.1.5",
|
||||
"@astrojs/vue": "^4.5.3",
|
||||
"@fullcalendar/core": "^6.1.17",
|
||||
"@fullcalendar/daygrid": "^6.1.17",
|
||||
"@fullcalendar/icalendar": "^6.1.17",
|
||||
"@fullcalendar/react": "^6.1.17",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@heroui/react": "2.7.6",
|
||||
"@logto/browser": "^2.2.18",
|
||||
"@heroui/react": "2.6.14",
|
||||
"@stylistic/eslint-plugin": "^2.8.0",
|
||||
"@stylistic/eslint-plugin": "^2.13.0",
|
||||
"astro": "^4.16.18",
|
||||
"framer-motion": "^11.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^11.18.2",
|
||||
"ical.js": "^1.5.0",
|
||||
"md5": "^2.3.0",
|
||||
"openapi-fetch": "^0.12.2",
|
||||
"openapi-fetch": "^0.12.5",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rehype": "^13.0.2",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"uuid": "10.0.0",
|
||||
"vue": "^3.5.10"
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/markdown-remark": "^5.2.0",
|
||||
"@cspell/eslint-plugin": "^8.14.4",
|
||||
"@eslint/js": "^9.11.1",
|
||||
"@hey-api/openapi-ts": "^0.53.5",
|
||||
"@astrojs/markdown-remark": "^5.3.0",
|
||||
"@cspell/eslint-plugin": "^8.19.2",
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@hey-api/openapi-ts": "^0.53.12",
|
||||
"@types/eslint__js": "^8.42.3",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react": "^18.3.20",
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"@types/uuid": "10.0.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-astro": "^1.2.4",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"globals": "^15.9.0",
|
||||
"husky": "^9.1.6",
|
||||
"lint-staged": "^15.2.10",
|
||||
"openapi-typescript": "^7.4.1",
|
||||
"typescript": "^5.6.2",
|
||||
"eslint-import-resolver-typescript": "^3.10.1",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"globals": "^15.15.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.1",
|
||||
"openapi-typescript": "^7.6.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "8.7.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
|
|||
10804
pnpm-lock.yaml
10804
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@ import HeaderNavigation from "./HeaderNavigation"
|
|||
|
||||
<nav class="nav">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="max-w-[1005px] w-full">
|
||||
<div class="w-full flex items-center">
|
||||
<HeaderNavigation client:load />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ export default function App() {
|
|||
name: "关于我们",
|
||||
},
|
||||
{
|
||||
link: "/join-us",
|
||||
name: "加入我们",
|
||||
link: "/repair",
|
||||
name: "维修",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Navbar onMenuOpenChange={setIsMenuOpen} height="48px">
|
||||
<NavbarContent className="flex justify-between items-center">
|
||||
<Navbar onMenuOpenChange={setIsMenuOpen} height="48px" className="">
|
||||
<NavbarContent className="flex justify-between items-center px-0 md:px-[22px]">
|
||||
<NavbarBrand className="flex gap-4">
|
||||
<img
|
||||
src="https://oss.nbtca.space/CA-logo.svg"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import type LogtoClient from "@logto/browser"
|
|||
|
||||
const logtoClient = ref<LogtoClient>()
|
||||
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 () => {
|
||||
logtoClient.value?.signOut(import.meta.env.PUBLIC_LOGTO_REDIRECT_URL)
|
||||
|
|
@ -34,9 +38,9 @@ onMounted(() => {
|
|||
})
|
||||
</script>
|
||||
<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="">
|
||||
<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 class="flex items-center" v-if="isAuthenticated">
|
||||
<Menu as="div" class="relative inline-block text-left">
|
||||
|
|
|
|||
17
src/components/header/RepairHeader.astro
Normal file
17
src/components/header/RepairHeader.astro
Normal 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>
|
||||
BIN
src/pages/_assets/repair_day_on_site.jpeg
Normal file
BIN
src/pages/_assets/repair_day_on_site.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 430 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 433 KiB |
|
|
@ -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="font-bold text-center">
|
||||
<div>
|
||||
扫一扫,
|
||||
遇到问题?
|
||||
</div>
|
||||
<div>
|
||||
获取免费维修。
|
||||
线上预约维修。
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="/repair/mini-program" class="text-base ">查看报修小程序{" >"}</a>
|
||||
<a href="/repair" class="text-base ">预约维修{" >"}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full bg-white rounded-2xl overflow-hidden flex flex-col items-center justify-center p-6 xl:p-8">
|
||||
|
|
|
|||
|
|
@ -2,22 +2,29 @@
|
|||
|
||||
<script>
|
||||
import { makeLogtoClient } from "../utils/auth"
|
||||
import LogtoClient from "@logto/browser"
|
||||
|
||||
const callbackHandler = async (logtoClient: LogtoClient) => {
|
||||
const callbackHandler = async (logtoClient) => {
|
||||
console.log("callbackHandler")
|
||||
try {
|
||||
await logtoClient.handleSignInCallback(window.location.href)
|
||||
if (!logtoClient.isAuthenticated) {
|
||||
console.log("User not authenticated")
|
||||
window.location.assign("/")
|
||||
return
|
||||
}
|
||||
// Handle successful sign-in
|
||||
window.location.assign("/")
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
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>
|
||||
|
|
|
|||
164
src/pages/repair/EventDetail.tsx
Normal file
164
src/pages/repair/EventDetail.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
238
src/pages/repair/TicketForm.tsx
Normal file
238
src/pages/repair/TicketForm.tsx
Normal 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>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
12
src/pages/repair/create-ticket.astro
Normal file
12
src/pages/repair/create-ticket.astro
Normal 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>
|
||||
83
src/pages/repair/index.astro
Normal file
83
src/pages/repair/index.astro
Normal 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>
|
||||
47
src/pages/repair/login-hint.astro
Normal file
47
src/pages/repair/login-hint.astro
Normal 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>
|
||||
|
|
@ -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>
|
||||
12
src/pages/repair/ticket-detail.astro
Normal file
12
src/pages/repair/ticket-detail.astro
Normal 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
78
src/types/event.ts
Normal 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",
|
||||
},
|
||||
]
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
import LogtoClient from "@logto/browser"
|
||||
|
||||
export const makeLogtoClient = () =>
|
||||
new LogtoClient({
|
||||
let logtoClient: LogtoClient | undefined = undefined
|
||||
|
||||
export const makeLogtoClient = () => {
|
||||
if (logtoClient === undefined) {
|
||||
logtoClient = new LogtoClient({
|
||||
endpoint: import.meta.env.PUBLIC_LOGTO_ENDPOINT,
|
||||
appId: import.meta.env.PUBLIC_LOGTO_APP_ID,
|
||||
scopes: ["email", "custom_data", "roles"],
|
||||
})
|
||||
}
|
||||
return logtoClient
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import createClient from "openapi-fetch"
|
||||
import type { paths as saturdayPaths } from "../types/saturday"
|
||||
// import type { paths as activePaths } from "../types/active"
|
||||
import { ApiClient } from "./active"
|
||||
|
||||
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({
|
||||
BASE: "https://active.nbtca.space",
|
||||
// BASE: "/active",
|
||||
})
|
||||
|
||||
export * from "./active/types.gen"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { heroui } from "@heroui/react"
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
|
||||
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}", "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
screens: {
|
||||
xs: "416px",
|
||||
sm: "734px",
|
||||
md: "1068px",
|
||||
lg: "1441px",
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#004b86",
|
||||
|
|
|
|||
Loading…
Reference in a new issue