This commit is contained in:
Clas Wen 2025-04-24 22:10:04 +08:00
parent 1fb4f9bdef
commit 3eb45180cf
17 changed files with 5718 additions and 5603 deletions

View file

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

View file

@ -14,53 +14,53 @@
"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",
"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": {

File diff suppressed because it is too large Load diff

View file

@ -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>

View file

@ -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"

View file

@ -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">

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

View file

@ -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>

View file

@ -0,0 +1,105 @@
---
layout: "../../../../../layouts/MarkdownPost.astro"
title: "Install the Archlinux (SpeedRun Version)"
pubDate: 2025-03-28
description: "arch速通纪录请在专业人士指导下操作XD"
author: "小明"
cover:
url: "https://www.svgrepo.com/show/349296/arch-linux.svg"
alt: "cover"
tags: ["技术"]
---
# 🚀 Arch Linux Dual Boot Guide (Speedrun Version)
## **1. Prepare Windows**
- **Shrink Windows Partition**
- Open `diskmgmt.msc`, shrink `C:` by **50GB+** (Unallocated Space).
- **Check Boot Mode**
- Press `Win + R`, type `msinfo32`, check **BIOS Mode**:
- **UEFI** → Continue with UEFI
- **Legacy (MBR)** → Use MBR installation
---
## **2. Boot into Arch Live USB**
- Create USB:
```sh
dd if=archlinux.iso of=/dev/sdX bs=4M status=progress
```
- Boot from USB in BIOS.
---
## **3. Partition the Disk**
```sh
lsblk # Identify your disk (e.g., /dev/nvme0n1)
cfdisk /dev/nvme0n1
```
- **Keep Windows partitions**
- Create **Linux Root Partition** (`ext4`, **50GB+**)
- **(Optional)** Create Swap Partition (8GB)
---
## **4. Format & Mount**
```sh
mkfs.ext4 /dev/nvme0n1p3
mount /dev/nvme0n1p3 /mnt
mkdir -p /mnt/boot/efi
mount /dev/nvme0n1p1 /mnt/boot/efi # Use Windows' EFI partition
```
*(If Swap)*
```sh
mkswap /dev/nvme0n1p4
swapon /dev/nvme0n1p4
```
---
## **5. Install Arch**
```sh
pacstrap /mnt base linux linux-firmware vim
genfstab -U /mnt >> /mnt/etc/fstab
arch-chroot /mnt
```
---
## **6. Basic Config**
```sh
ln -sf /usr/share/zoneinfo/Region/City /etc/localtime
hwclock --systohc
echo "LANG=en_US.UTF-8" > /etc/locale.conf
echo "archlinux" > /etc/hostname
passwd # Set root password
```
---
## **7. Install & Configure GRUB**
```sh
pacman -S grub efibootmgr os-prober
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB
os-prober
grub-mkconfig -o /boot/grub/grub.cfg
```
---
## **8. Finish Installation**
```sh
exit
umount -R /mnt
reboot
```
- **Select Arch Linux from GRUB** 🎉
- If Windows is missing from GRUB:
```sh
os-prober
grub-mkconfig -o /boot/grub/grub.cfg
```
---
## **Done!** 🚀

View file

@ -0,0 +1,10 @@
---
import NavigationUser from "../../components/header/NavigationUser.vue"
---
<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 class="font-semibold">维修</span>
<NavigationUser client:load />
</div>
</div>

View file

@ -0,0 +1,131 @@
import { useEffect, useState } from "react"
import { makeLogtoClient } from "../../utils/auth"
import type { UserInfoResponse } from "@logto/browser"
import { Form, Input, Button, Textarea } from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import type LogtoClient from "@logto/browser"
export default function App() {
const [userInfo, setUserInfo] = useState<UserInfoResponse>()
const [formData, setFormData] = useState<{
model?: string
phone?: string
qq?: string
description?: string
}>({})
let logtoClient: LogtoClient | undefined = undefined
useEffect(() => {
const check = async () => {
logtoClient = makeLogtoClient()
const createRepairPath = "/repair/create-ticket"
const authenticated = await logtoClient.isAuthenticated()
if (!authenticated) {
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
return
}
await checkAuth()
const res = await logtoClient.getIdTokenClaims()
setUserInfo(res)
}
check()
}, [])
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const logtoToken = await logtoClient.getAccessToken()
await saturdayClient.POST("/client/event", {
headers: {
Authorization: `Bearer ${logtoToken}`,
},
body: {
Problem: formData.description,
model: formData.model,
phone: formData.phone,
qq: formData.qq,
},
})
}
return (
<section className="box-border mb-24">
<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={userInfo?.email || ""}
readOnly
description="我们会向此邮箱发送维修相关的通知"
/>
<Input
label="电话号码"
placeholder="必填"
errorMessage="电话号码不能为空"
isRequired
type="phone"
name="phone"
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 })
}}
/>
<Button type="submit" color="primary" className="my-4 w-full">
</Button>
</Form>
</div>
</section>
)
}

View file

@ -0,0 +1,12 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro"
import RepairHeader from "./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 "./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 "./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,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
}

View file

@ -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"

View file

@ -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",