mirror of
https://github.com/m1ngsama/FUJI.git
synced 2025-12-24 10:51:27 +00:00
Merge branch 'main' into dependabot/npm_and_yarn/npm_and_yarn-82a5d800d0
This commit is contained in:
commit
41648136df
14 changed files with 19775 additions and 90 deletions
14
.github/workflows/main.yml
vendored
14
.github/workflows/main.yml
vendored
|
|
@ -72,16 +72,4 @@ jobs:
|
|||
platforms: linux/amd64
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME_FULL }}:${{ github.ref_name }}
|
||||
|
||||
- name: Deploy
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
env:
|
||||
BRANCH_NAME: ${{ github.ref_name }}
|
||||
with:
|
||||
host: ${{ secrets.REMOTE_HOST }}
|
||||
username: ${{ secrets.REMOTE_USER }}
|
||||
key: ${{ secrets.ACCESS_TOKEN }}
|
||||
envs: BRANCH_NAME
|
||||
script: |
|
||||
cd ${{secrets.REMOTE_PATH}}
|
||||
docker compose up --force-recreate -d --pull=always
|
||||
${{ env.IMAGE_NAME_FULL }}:latest
|
||||
|
|
|
|||
18926
package-lock.json
generated
Normal file
18926
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,11 @@ import GithubMark from "./assets/github-mark.svg"
|
|||
export default function App() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||
|
||||
const menuItems = [
|
||||
const menuItems: {
|
||||
link: string
|
||||
name: string
|
||||
target?: string
|
||||
}[] = [
|
||||
{
|
||||
link: "/blog",
|
||||
name: "博客",
|
||||
|
|
@ -20,6 +24,11 @@ export default function App() {
|
|||
link: "/repair",
|
||||
name: "维修",
|
||||
},
|
||||
{
|
||||
link: "https://docs.nbtca.space",
|
||||
name: "文档",
|
||||
target: "_blank",
|
||||
},
|
||||
{
|
||||
link: "/about",
|
||||
name: "关于我们",
|
||||
|
|
@ -50,8 +59,17 @@ export default function App() {
|
|||
{
|
||||
menuItems.map(item => (
|
||||
<NavbarItem key={item.name}>
|
||||
<Link color="foreground" className="nav-item-content hover:text-[#2997ff] text-nowrap" href={item.link}>
|
||||
{item.name}
|
||||
<Link color="foreground" className="nav-item-content hover:text-[#2997ff] text-nowrap flex items-center" href={item.link} target={item.target || "_self"}>
|
||||
<span>
|
||||
{item.name}
|
||||
</span>
|
||||
{
|
||||
item.target == "_blank" && (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-4 ml-0.5 inline-block">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
</Link>
|
||||
</NavbarItem>
|
||||
))
|
||||
|
|
@ -78,8 +96,18 @@ export default function App() {
|
|||
className="w-full py-1 font-bold"
|
||||
href={item.link}
|
||||
size="lg"
|
||||
target={item.target || "_self"}
|
||||
>
|
||||
{item.name}
|
||||
<span>
|
||||
{item.name}
|
||||
</span>
|
||||
{
|
||||
item.target == "_blank" && (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" className="size-5 ml-1 inline-block">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
</Link>
|
||||
</NavbarMenuItem>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,134 @@
|
|||
<div></div>
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro"
|
||||
import RepairHeader from "../components/header/RepairHeader.astro"
|
||||
---
|
||||
|
||||
<BaseLayout primaryTitle="Authentication Callback">
|
||||
<RepairHeader></RepairHeader>
|
||||
<section class="min-h-[70vh]">
|
||||
<div class="section-content my-16 flex flex-col gap-8">
|
||||
<div>
|
||||
<div>
|
||||
<div id="status-text" class="text-xl font-bold">处理登录中...</div>
|
||||
<div id="status-subtitle" class="text-sm text-gray-700">
|
||||
正在验证身份
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div id="loading-spinner" class="flex justify-center my-8">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
<div id="status-message" class="text-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="continue-button-wrapper" class="mt-4 hidden">
|
||||
<button
|
||||
id="continue-button"
|
||||
class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors font-medium"
|
||||
>
|
||||
继续
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.text-success {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { makeLogtoClient } from "../utils/auth"
|
||||
|
||||
const statusText = document.getElementById("status-text")
|
||||
const statusSubtitle = document.getElementById("status-subtitle")
|
||||
const statusMessage = document.getElementById("status-message")
|
||||
const continueButtonWrapper = document.getElementById("continue-button-wrapper")
|
||||
const continueButton = document.getElementById("continue-button")
|
||||
const loadingSpinner = document.getElementById("loading-spinner")
|
||||
|
||||
const updateStatus = (title, subtitle, message, isError = false) => {
|
||||
statusText.textContent = title
|
||||
statusSubtitle.textContent = subtitle
|
||||
statusMessage.textContent = message
|
||||
|
||||
if (isError) {
|
||||
statusText.classList.add("text-error")
|
||||
statusText.classList.remove("text-success")
|
||||
}
|
||||
else {
|
||||
statusText.classList.add("text-success")
|
||||
statusText.classList.remove("text-error")
|
||||
}
|
||||
}
|
||||
|
||||
const showContinueButton = () => {
|
||||
loadingSpinner.classList.add("hidden")
|
||||
continueButtonWrapper.classList.remove("hidden")
|
||||
}
|
||||
|
||||
const redirectToHome = () => {
|
||||
window.location.assign("/")
|
||||
}
|
||||
|
||||
continueButton.addEventListener("click", redirectToHome)
|
||||
|
||||
// Show manual continue button after 5 seconds as fallback
|
||||
const fallbackTimer = setTimeout(() => {
|
||||
updateStatus(
|
||||
"等待时间较长...",
|
||||
"如果未自动跳转,请手动继续",
|
||||
"你可以点击下方按钮手动继续。",
|
||||
false,
|
||||
)
|
||||
showContinueButton()
|
||||
}, 5000)
|
||||
|
||||
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("/")
|
||||
clearTimeout(fallbackTimer)
|
||||
updateStatus(
|
||||
"认证失败",
|
||||
"无法验证你的登录",
|
||||
"无法验证你的登录信息。正在跳转到首页...",
|
||||
true,
|
||||
)
|
||||
showContinueButton()
|
||||
setTimeout(redirectToHome, 2000)
|
||||
return
|
||||
}
|
||||
|
||||
// Authentication successful
|
||||
clearTimeout(fallbackTimer)
|
||||
updateStatus(
|
||||
"登录成功!",
|
||||
"验证完成",
|
||||
"正在跳转到首页...",
|
||||
)
|
||||
showContinueButton()
|
||||
setTimeout(redirectToHome, 1500)
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
window.location.assign("/")
|
||||
clearTimeout(fallbackTimer)
|
||||
updateStatus(
|
||||
"认证错误",
|
||||
"登录过程中出现错误",
|
||||
`登录过程中出现错误:${error.message || "未知错误"}。点击下方按钮返回首页。`,
|
||||
true,
|
||||
)
|
||||
showContinueButton()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -26,5 +139,13 @@ try {
|
|||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
clearTimeout(fallbackTimer)
|
||||
updateStatus(
|
||||
"初始化错误",
|
||||
"无法初始化认证客户端",
|
||||
"无法初始化认证客户端。点击下方按钮返回首页。",
|
||||
true,
|
||||
)
|
||||
showContinueButton()
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export default function JoinForm() {
|
|||
const [loading, setLoading] = useState(false)// 添加加载状态
|
||||
const [popoverOpen, setPopoverOpen] = useState(false)// 控制 Popover 显示
|
||||
const [popoverMessage, setPopoverMessage] = useState("")// Popover 显示的消息
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})// 表单验证错误
|
||||
|
||||
// 在组件挂载时加载本地存储的数据
|
||||
useEffect(() => {
|
||||
|
|
@ -45,10 +46,60 @@ export default function JoinForm() {
|
|||
// 处理表单输入变化
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prevData => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}))
|
||||
|
||||
// 如果是邮箱字段,检查是否是QQ邮箱并自动填充QQ号
|
||||
if (name === "email") {
|
||||
const qqEmailMatch = value.match(/^(\d+)@qq\.com$/i)
|
||||
if (qqEmailMatch) {
|
||||
const qqNumber = qqEmailMatch[1]
|
||||
setFormData(prevData => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
qq: qqNumber, // 自动填充QQ号
|
||||
}))
|
||||
}
|
||||
else {
|
||||
setFormData(prevData => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}))
|
||||
}
|
||||
}
|
||||
// 如果是QQ字段,检查是否需要自动填充邮箱
|
||||
else if (name === "qq") {
|
||||
setFormData((prevData) => {
|
||||
const newData = {
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}
|
||||
|
||||
// 只有在邮箱为空或者邮箱是数字@qq.com格式时,才自动填充
|
||||
const isEmailEmpty = !prevData.email || prevData.email.trim() === ""
|
||||
const isEmailQQFormat = /^\d+@qq\.com$/i.test(prevData.email)
|
||||
|
||||
// 如果QQ号是纯数字且满足条件,自动填充邮箱
|
||||
if (value && /^\d+$/.test(value) && (isEmailEmpty || isEmailQQFormat)) {
|
||||
newData.email = `${value}@qq.com`
|
||||
}
|
||||
|
||||
return newData
|
||||
})
|
||||
}
|
||||
else {
|
||||
setFormData(prevData => ({
|
||||
...prevData,
|
||||
[name]: value,
|
||||
}))
|
||||
}
|
||||
|
||||
// 清除该字段的错误
|
||||
if (errors[name]) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev }
|
||||
delete newErrors[name]
|
||||
return newErrors
|
||||
})
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
saveToLocalStorage()
|
||||
|
|
@ -59,8 +110,51 @@ export default function JoinForm() {
|
|||
}, 100)
|
||||
}
|
||||
|
||||
// 验证表单
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
|
||||
// 姓名验证
|
||||
if (!formData.name || formData.name.trim().length === 0) {
|
||||
newErrors.name = "姓名不能为空"
|
||||
}
|
||||
|
||||
// 学号验证
|
||||
if (!formData.number || formData.number.trim().length === 0) {
|
||||
newErrors.number = "学号不能为空"
|
||||
}
|
||||
|
||||
// 专业验证
|
||||
if (!formData.major || formData.major.trim().length === 0) {
|
||||
newErrors.major = "专业不能为空"
|
||||
}
|
||||
|
||||
// 班级验证
|
||||
if (!formData.class || formData.class.trim().length === 0) {
|
||||
newErrors.class = "班级不能为空"
|
||||
}
|
||||
|
||||
// 邮箱验证
|
||||
if (!formData.email || formData.email.trim().length === 0) {
|
||||
newErrors.email = "邮箱不能为空"
|
||||
}
|
||||
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = "邮箱格式不正确"
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = async () => {
|
||||
// 验证表单
|
||||
if (!validateForm()) {
|
||||
setPopoverMessage("请填写所有必填项并确保格式正确")
|
||||
setPopoverOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)// 设置加载状态为 true
|
||||
try {
|
||||
await activeClient.freshman.postFreshmanAdd({
|
||||
|
|
@ -96,6 +190,8 @@ export default function JoinForm() {
|
|||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
isInvalid={!!errors.name}
|
||||
errorMessage={errors.name}
|
||||
/>
|
||||
<Input
|
||||
name="class"
|
||||
|
|
@ -103,6 +199,8 @@ export default function JoinForm() {
|
|||
value={formData.class}
|
||||
onChange={handleChange}
|
||||
required
|
||||
isInvalid={!!errors.class}
|
||||
errorMessage={errors.class}
|
||||
/>
|
||||
<Input
|
||||
name="number"
|
||||
|
|
@ -110,6 +208,8 @@ export default function JoinForm() {
|
|||
value={formData.number}
|
||||
onChange={handleChange}
|
||||
required
|
||||
isInvalid={!!errors.number}
|
||||
errorMessage={errors.number}
|
||||
/>
|
||||
<Input
|
||||
name="major"
|
||||
|
|
@ -117,6 +217,8 @@ export default function JoinForm() {
|
|||
value={formData.major}
|
||||
onChange={handleChange}
|
||||
required
|
||||
isInvalid={!!errors.major}
|
||||
errorMessage={errors.major}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
|
|
@ -124,25 +226,25 @@ export default function JoinForm() {
|
|||
我们需要以下信息以便联系你
|
||||
</div>
|
||||
<Input
|
||||
name="phone"
|
||||
placeholder="电话"
|
||||
value={formData.phone}
|
||||
name="email"
|
||||
placeholder="邮箱"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
isInvalid={!!errors.email}
|
||||
errorMessage={errors.email}
|
||||
/>
|
||||
<Input
|
||||
name="qq"
|
||||
placeholder="QQ"
|
||||
value={formData.qq}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
name="email"
|
||||
placeholder="邮箱"
|
||||
value={formData.email}
|
||||
name="phone"
|
||||
placeholder="电话"
|
||||
value={formData.phone}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
|
|
|
|||
BIN
src/pages/posts/_assets/github_markdown.png
Normal file
BIN
src/pages/posts/_assets/github_markdown.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
13
src/pages/posts/_assets/github_markdown.svg
Normal file
13
src/pages/posts/_assets/github_markdown.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg width="400" height="394" viewBox="0 0 400 394" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="393.469" fill="white"/>
|
||||
<g clip-path="url(#clip0_1_2)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M199.523 40C111.311 40 40 111.837 40 200.709C40 271.749 85.6914 331.882 149.078 353.166C157.002 354.766 159.905 349.708 159.905 345.453C159.905 341.727 159.644 328.957 159.644 315.651C115.269 325.231 106.028 296.493 106.028 296.493C98.8963 277.868 88.3298 273.081 88.3298 273.081C73.8057 263.236 89.3878 263.236 89.3878 263.236C105.499 264.3 113.953 279.732 113.953 279.732C128.212 304.209 151.19 297.293 160.434 293.035C161.753 282.658 165.982 275.474 170.472 271.484C135.079 267.758 97.8416 253.923 97.8416 192.193C97.8416 174.632 104.176 160.264 114.214 149.091C112.63 145.1 107.082 128.601 115.801 106.518C115.801 106.518 129.27 102.26 159.641 123.014C172.644 119.496 186.053 117.706 199.523 117.691C212.993 117.691 226.723 119.556 239.402 123.014C269.776 102.26 283.246 106.518 283.246 106.518C291.964 128.601 286.413 145.1 284.829 149.091C295.131 160.264 301.205 174.632 301.205 192.193C301.205 253.923 263.967 267.491 228.31 271.484C234.122 276.539 239.138 286.116 239.138 301.283C239.138 322.834 238.877 340.131 238.877 345.45C238.877 349.708 241.783 354.766 249.704 353.169C313.091 331.879 358.782 271.749 358.782 200.709C359.043 111.837 287.471 40 199.523 40Z" fill="#24292F"/>
|
||||
</g>
|
||||
<path d="M147.618 156.47C141.782 156.47 137 161.337 137 167.151V225.093C137 230.907 141.782 235.774 147.617 235.774H251.719C257.554 235.774 262.336 230.907 262.336 225.093V167.152C262.336 161.338 257.553 156.471 251.719 156.471H150.566L150.564 156.469L147.618 156.47ZM147.611 166.013H251.719C252.344 166.013 252.795 166.436 252.795 167.153V225.093C252.795 225.81 252.342 226.233 251.719 226.233H147.617C146.994 226.233 146.541 225.81 146.541 225.093V167.151C146.541 166.436 146.992 166.016 147.611 166.013Z" fill="#24292F"/>
|
||||
<path d="M156.389 216.222V176.022H168.088L179.785 190.802L191.478 176.022H203.174V216.222H191.478V193.166L179.784 207.946L168.086 193.166V216.222H156.389ZM229.493 216.222L211.948 196.713H223.645V176.022H235.343V196.712H247.041L229.493 216.222Z" fill="#24292F"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_1_2">
|
||||
<rect width="320" height="313.469" fill="white" transform="translate(40 40)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
206
src/pages/posts/nbtca-post.md
Normal file
206
src/pages/posts/nbtca-post.md
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
---
|
||||
layout: "../../layouts/MarkdownPost.astro"
|
||||
title: "NBTCA主页投稿指南1.0"
|
||||
pubDate: 2025-10-10
|
||||
description: "在开源项目中发布你的第一篇博客"
|
||||
author:
|
||||
name: "小明"
|
||||
url: "https://blog.m1ng.space/"
|
||||
cover:
|
||||
url: ./_assets/github_markdown.png
|
||||
alt: "cover"
|
||||
tags: ["指南", "Git", "Markdown"]
|
||||
---
|
||||
|
||||
##
|
||||
|
||||
> 学会使用 Git + Markdown 撰写与提交技术博客
|
||||
|
||||
## 一、前言
|
||||
|
||||
本指南将指导你如何使用最主流的开源协作方式——**Git + Markdown + Pull Request**,来撰写并发布你的第一篇NBTCA博客。
|
||||
|
||||
目标是:
|
||||
|
||||
> 让每位新社员都能独立完成一篇博客投稿流程。
|
||||
|
||||
---
|
||||
|
||||
## 二、准备工作
|
||||
|
||||
### 1. 安装 Git
|
||||
|
||||
#### Windows
|
||||
|
||||
前往 [https://git-scm.com/downloads](https://git-scm.com/downloads) 下载并安装,保持默认选项即可。
|
||||
|
||||
#### macOS
|
||||
|
||||
安装命令行工具集,使用brew安装git
|
||||
|
||||
```bash
|
||||
xcode-select --install
|
||||
brew install git
|
||||
```
|
||||
|
||||
#### Linux(例如 Ubuntu / Arch)
|
||||
|
||||
使用对应发行版的包管理器安装git
|
||||
|
||||
```bash
|
||||
sudo apt install git
|
||||
# 或
|
||||
sudo pacman -S git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 注册 GitHub 账号
|
||||
|
||||
访问 [https://github.com](https://github.com),注册并登录,设置一个好记的用户名。
|
||||
|
||||
---
|
||||
|
||||
### 3. 基础配置
|
||||
|
||||
```bash
|
||||
git config --global user.name "你的名字"
|
||||
git config --global user.email "你的邮箱"
|
||||
```
|
||||
|
||||
> 当然,你也可以使用[github-cli](https://github.com/cli/cli)来完成github的认证过程,但是[git的工作流程](https://nbtca.space/posts/blogs/Tech/Git/git-book-1)还是必要掌握的
|
||||
|
||||
---
|
||||
|
||||
## 三、Fork 与 Clone 以及目前协会博客仓库的贡献方法
|
||||
|
||||
一般的工作流程是将源代码仓库[Fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo)一份到自己名下创建一个新的下游仓库,在自己的下游仓库编写代码并通过[创建pr](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)的方式提交更新到上游仓库。
|
||||
|
||||
目前NBTCA的[Home项目](https://github.com/nbtca/home)集成了[CI/CD](https://github.com/resources/articles/ci-cd)
|
||||
|
||||
为了保证交付安全,默认只有项目源代码仓库的分支提交的pr会触发[github action](https://github.com/features/actions),从下游仓库提交的pr在合并后并不会触发构建,这一点需要注意,所以推荐在源代码的基础上创建分支并pr
|
||||
|
||||
### 2. Clone
|
||||
|
||||
```bash
|
||||
git clone https://github.com/nbtca/home.git
|
||||
cd blog
|
||||
# 如果是gh-cli则是gh repo clone nbtca/home
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、创建分支
|
||||
|
||||
```bash
|
||||
git checkout -b add-my-first-blog
|
||||
# -b 参数代表创建一个新的分支
|
||||
# 此处add-my-first-blog作为分支名可以自行替换,
|
||||
# 我个人的习惯是提交类型+具体事务类型,例如post/blog-post、feature/homepage等。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、撰写博客(Markdown 格式)
|
||||
|
||||
### 1. 新建文件
|
||||
|
||||
在 `src/pages/posts/` 文件夹中新建:
|
||||
|
||||
```
|
||||
my-first-blog.md
|
||||
# 换成你喜欢的名字,最好是英文的方便管理
|
||||
```
|
||||
|
||||
### 2. 文件模板
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: "../../layouts/MarkdownPost.astro"
|
||||
title: "题目"
|
||||
pubDate: 2025-10-10
|
||||
description: "描述"
|
||||
author: "张三
|
||||
cover:
|
||||
url: "封面地址url,也可以引用本地图片"
|
||||
alt: "cover"
|
||||
tags: ["标签", "可多个"]
|
||||
---
|
||||
|
||||
# Git 与 Markdown 入门指南
|
||||
|
||||
大家好,我是计算机协会新社员张三。
|
||||
|
||||
本文将介绍如何使用 Git 与 Markdown 撰写并提交博客。
|
||||
|
||||
## 一、Git 是什么?
|
||||
|
||||
Git 是一个分布式版本控制系统,用于多人协作开发。
|
||||
|
||||
## 二、Markdown 是什么?
|
||||
|
||||
Markdown 是一种轻量级标记语言,用简单的符号来排版文字。
|
||||
|
||||
- **加粗**:`**加粗**`
|
||||
- _斜体_:`*斜体*`
|
||||
- 链接:`[协会官网](https://example.com)`
|
||||
|
||||
## 三、总结
|
||||
|
||||
学会使用 Git + Markdown,你就能参与到开源协作中了!
|
||||
```
|
||||
|
||||
> 以上为行文推荐格式,关于[markdown](https://www.markdownguide.org/)的写法可自行查阅手册。
|
||||
|
||||
---
|
||||
|
||||
## 六、提交与推送
|
||||
|
||||
```bash
|
||||
git add my-first-blog.md
|
||||
# 将更新的文件添加到暂存区
|
||||
|
||||
git commit -m "Add my first blog: Git 与 Markdown 入门"
|
||||
# 将暂存区的文件集合为一次提交,并对本次提交做出说明
|
||||
|
||||
git push origin add-my-first-blog
|
||||
# 将提交从本地同步到远程Github仓库,提交到远程仓库的对应新分支
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、创建 Pull Request(PR)
|
||||
|
||||
1. 打开你的 GitHub 仓库。
|
||||
2. 点击 “**Compare & pull request**”。
|
||||
3. 填写标题与说明(例如 “新增一篇关于 Git 的博客”)。
|
||||
4. 目标仓库选择 `nbtca/home`。
|
||||
5. 提交 Pull Request。
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| ------------------- | ------------------------------------------------------ |
|
||||
| push 时提示拒绝访问 | 检查是否使用 HTTPS 地址,并确认你已登录 GitHub。 |
|
||||
| 提交重复文件或出错 | 使用 `git status` 查看状态,`git reset` 撤销错误提交。 |
|
||||
| PR 没被合并 | 可能格式不规范,等待管理员审核反馈。 |
|
||||
|
||||
---
|
||||
|
||||
## 九、推荐工具
|
||||
|
||||
- 编辑器:**VS Code**、**Neovim**、**Typora**
|
||||
- Markdown 预览插件:_Markdown Preview Enhanced_
|
||||
- Git 图形界面工具:_GitHub Desktop_、_Sourcetree_
|
||||
|
||||
---
|
||||
|
||||
## 十、结语
|
||||
|
||||
当仓库管理员[Review](https://github.com/features/code-review)代码后,代码就可以[Merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/merging-a-pull-request)了
|
||||
|
||||
当你第一次成功合并 PR 时:
|
||||
|
||||
> 恭喜你,🎉 你正式成为了开源协作的一员!
|
||||
|
|
@ -267,7 +267,7 @@ export const getAvailableEventActions = (event: PublicEvent, identityContext: Id
|
|||
action: "drop",
|
||||
label: "放弃",
|
||||
handler: async () => {
|
||||
const { data } = await saturdayClient.POST("/member/events/{EventId}/accept", {
|
||||
const { data } = await saturdayClient.DELETE("/member/events/{EventId}/accept", {
|
||||
params: {
|
||||
header: {
|
||||
Authorization: `Bearer ${identityContext.token}`,
|
||||
|
|
|
|||
|
|
@ -188,15 +188,32 @@ export const validateRepairRole = (roles: string[]) => {
|
|||
|
||||
export default function App() {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
// Initialize state from URL query params
|
||||
const getInitialPage = () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const pageParam = params.get('page')
|
||||
return pageParam ? parseInt(pageParam, 10) : 1
|
||||
}
|
||||
|
||||
const getInitialStatusFilter = () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const statusParam = params.get('status')
|
||||
if (statusParam) {
|
||||
return statusParam.split(',').filter(Boolean)
|
||||
}
|
||||
return UserEventStatus.filter(v => v.status !== EventStatus.cancelled).map(v => v.status)
|
||||
}
|
||||
|
||||
const [page, setPage] = useState(getInitialPage())
|
||||
const rowsPerPage = 10
|
||||
const [statusFilter, setStatusFilter] = useState<string[]>(
|
||||
UserEventStatus.filter(v => v.status !== EventStatus.cancelled).map(v => v.status),
|
||||
)
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const [statusFilter, setStatusFilter] = useState<string[]>(getInitialStatusFilter())
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure()
|
||||
const [userInfo, setUserInfo] = useState<UserInfoResponse>()
|
||||
const [currentMember, setCurrentMember] = useState<PublicMember>()
|
||||
const [token, setToken] = useState<string>()
|
||||
const [errorMessage, setErrorMessage] = useState<string>("")
|
||||
|
||||
useEffect(() => {
|
||||
const check = async () => {
|
||||
|
|
@ -228,18 +245,60 @@ export default function App() {
|
|||
check()
|
||||
}, [])
|
||||
|
||||
// Handle eventid query parameter to auto-open event detail
|
||||
useEffect(() => {
|
||||
const loadEventFromUrl = async () => {
|
||||
if (!token) return // Wait for authentication
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const eventId = params.get('eventid')
|
||||
|
||||
if (eventId) {
|
||||
try {
|
||||
const { data, error } = await saturdayClient.GET("/events/{eventId}", {
|
||||
params: {
|
||||
path: {
|
||||
eventId: eventId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (error || !data) {
|
||||
setErrorMessage(`无法找到工单 #${eventId},该工单可能不存在或已被删除`)
|
||||
} else {
|
||||
setActiveEvent(data as PublicEvent)
|
||||
onOpen()
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorMessage(`加载工单 #${eventId} 时出错`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadEventFromUrl()
|
||||
}, [token])
|
||||
|
||||
const list = useAsyncList<PublicEvent>({
|
||||
async load() {
|
||||
const { data } = await saturdayClient.GET("/events", {
|
||||
setIsLoading(true)
|
||||
const offset = (page - 1) * rowsPerPage
|
||||
const { data, response } = await saturdayClient.GET("/events", {
|
||||
params: {
|
||||
query: {
|
||||
order: "DESC",
|
||||
offset: 0,
|
||||
limit: 1000,
|
||||
offset: offset,
|
||||
limit: rowsPerPage,
|
||||
status: statusFilter.length > 0 ? statusFilter : null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Extract total count from response headers
|
||||
const totalCountHeader = response.headers.get("X-Total-Count")
|
||||
if (totalCountHeader) {
|
||||
setTotalCount(parseInt(totalCountHeader, 10))
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
return {
|
||||
|
|
@ -263,28 +322,37 @@ export default function App() {
|
|||
},
|
||||
})
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
if (statusFilter.length > 0) {
|
||||
return list.items.filter(item => statusFilter.includes(item.status))
|
||||
}
|
||||
return list.items
|
||||
}, [list, statusFilter])
|
||||
|
||||
// Items are now paginated and filtered by the server
|
||||
const items = useMemo(() => {
|
||||
const start = (page - 1) * rowsPerPage
|
||||
const end = start + rowsPerPage
|
||||
|
||||
return filteredList.slice(start, end)
|
||||
}, [filteredList, page, rowsPerPage])
|
||||
return list.items
|
||||
}, [list.items])
|
||||
|
||||
const pages = useMemo(() => {
|
||||
return Math.ceil(filteredList.length / rowsPerPage)
|
||||
}, [filteredList, rowsPerPage])
|
||||
return Math.ceil(totalCount / rowsPerPage)
|
||||
}, [totalCount, rowsPerPage])
|
||||
|
||||
// Update URL query params when page or statusFilter changes
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('page', page.toString())
|
||||
if (statusFilter.length > 0) {
|
||||
params.set('status', statusFilter.join(','))
|
||||
} else {
|
||||
params.delete('status')
|
||||
}
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
}, [page, statusFilter])
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
list.reload()
|
||||
}, [statusFilter])
|
||||
|
||||
useEffect(() => {
|
||||
list.reload()
|
||||
}, [page])
|
||||
|
||||
const columns: {
|
||||
key: string
|
||||
label: string
|
||||
|
|
@ -335,6 +403,24 @@ export default function App() {
|
|||
const onOpenEventDetail = (event: PublicEvent) => {
|
||||
setActiveEvent(event)
|
||||
onOpen()
|
||||
|
||||
// Update URL with eventid
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('eventid', event.eventId)
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
}
|
||||
|
||||
const handleDrawerOpenChange = (isOpen: boolean) => {
|
||||
onOpenChange()
|
||||
|
||||
// Remove eventid from URL when drawer is closed
|
||||
if (!isOpen) {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.delete('eventid')
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const MobileEventCard = ({ event }: { event: PublicEvent }) => (
|
||||
|
|
@ -454,24 +540,26 @@ export default function App() {
|
|||
<CheckboxPopover value={statusFilter} onValueChange={setStatusFilter} />
|
||||
</div>
|
||||
|
||||
{isLoading
|
||||
? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Spinner label="Loading..." />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-col gap-4">
|
||||
{items.map(event => (
|
||||
<MobileEventCard key={event.eventId} event={event} />
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
暂无维修记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-h-[600px]">
|
||||
{isLoading
|
||||
? (
|
||||
<div className="flex justify-center py-8">
|
||||
<Spinner label="Loading..." />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-col gap-4">
|
||||
{items.map(event => (
|
||||
<MobileEventCard key={event.eventId} event={event} />
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
暂无维修记录
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile Pagination */}
|
||||
<div className="mt-6 flex justify-center">
|
||||
|
|
@ -531,14 +619,36 @@ export default function App() {
|
|||
}
|
||||
}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
onClose={() => {
|
||||
onOpenChange()
|
||||
}}
|
||||
onOpenChange={handleDrawerOpenChange}
|
||||
onClose={() => handleDrawerOpenChange(false)}
|
||||
onDelete={() => {}}
|
||||
onEdit={() => {}}
|
||||
>
|
||||
</TicketDetailDrawer>
|
||||
|
||||
{/* Error Message Display */}
|
||||
{errorMessage && (
|
||||
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 max-w-md w-full mx-4">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 shadow-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-red-600 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-red-800">{errorMessage}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setErrorMessage("")}
|
||||
className="text-red-400 hover:text-red-600 flex-shrink-0"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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 { safe } from "../../utils/safe"
|
||||
|
||||
type TicketFormData = {
|
||||
model?: string
|
||||
|
|
@ -275,12 +276,11 @@ export default function App() {
|
|||
const check = async () => {
|
||||
const createRepairPath = "/repair/create-ticket"
|
||||
try {
|
||||
const authenticated = await makeLogtoClient().isAuthenticated()
|
||||
if (!authenticated) {
|
||||
const [res, err] = await safe(makeLogtoClient().getIdTokenClaims())
|
||||
if (err) {
|
||||
window.location.href = `/repair/login-hint?redirectUrl=${createRepairPath}`
|
||||
return
|
||||
}
|
||||
const res = await makeLogtoClient().getIdTokenClaims()
|
||||
setUserInfo(res)
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,41 @@ body.theme-dark .astro-code span {
|
|||
background-color: var(--gray-primary) !important;
|
||||
}
|
||||
|
||||
/* Improve markdown hyperlink visibility */
|
||||
.prose a {
|
||||
color: #0071e3 !important;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: rgba(0, 113, 227, 0.4);
|
||||
text-underline-offset: 2px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.prose a:hover {
|
||||
color: #0077ed !important;
|
||||
text-decoration-color: #0077ed;
|
||||
}
|
||||
|
||||
.prose a:visited {
|
||||
color: #551A8B !important;
|
||||
text-decoration-color: rgba(85, 26, 139, 0.4);
|
||||
}
|
||||
|
||||
/* Dark mode link styles */
|
||||
body.theme-dark .prose a {
|
||||
color: #2997ff !important;
|
||||
text-decoration-color: rgba(41, 151, 255, 0.4);
|
||||
}
|
||||
|
||||
body.theme-dark .prose a:hover {
|
||||
color: #409cff !important;
|
||||
text-decoration-color: #409cff;
|
||||
}
|
||||
|
||||
body.theme-dark .prose a:visited {
|
||||
color: #bf5af2 !important;
|
||||
text-decoration-color: rgba(191, 90, 242, 0.4);
|
||||
}
|
||||
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
|
|
|
|||
167
src/types/saturday.d.ts
vendored
167
src/types/saturday.d.ts
vendored
|
|
@ -797,6 +797,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -842,6 +847,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -880,6 +890,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -918,6 +933,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -960,6 +980,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -992,6 +1017,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1025,6 +1055,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1055,7 +1090,7 @@ export interface operations {
|
|||
* @example 50
|
||||
*/
|
||||
limit?: number
|
||||
status?: string
|
||||
status?: string[] | null
|
||||
order?: string
|
||||
}
|
||||
header?: never
|
||||
|
|
@ -1067,6 +1102,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1087,16 +1127,6 @@ export interface operations {
|
|||
"export-events-xlsx": {
|
||||
parameters: {
|
||||
query: {
|
||||
/**
|
||||
* @description Offset
|
||||
* @example 0
|
||||
*/
|
||||
offset?: number
|
||||
/**
|
||||
* @description Limit
|
||||
* @example 50
|
||||
*/
|
||||
limit?: number
|
||||
status?: string
|
||||
order?: string
|
||||
start_time: string
|
||||
|
|
@ -1143,6 +1173,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1181,6 +1216,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1219,6 +1259,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1251,6 +1296,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1287,6 +1337,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1323,6 +1378,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1359,6 +1419,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1404,6 +1469,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1442,6 +1512,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1480,6 +1555,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1518,6 +1598,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1560,6 +1645,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1602,6 +1692,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1633,6 +1728,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1673,6 +1773,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1709,6 +1814,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1752,6 +1862,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1787,6 +1902,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1829,6 +1949,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1871,6 +1996,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1912,6 +2042,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1951,6 +2086,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
@ -1980,6 +2120,11 @@ export interface operations {
|
|||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
"X-Limit"?: number | null
|
||||
"X-Offset"?: number | null
|
||||
"X-Page"?: number | null
|
||||
"X-Total-Count"?: number | null
|
||||
"X-Total-Pages"?: number | null
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
|
|
|
|||
11
src/utils/safe.ts
Normal file
11
src/utils/safe.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export async function safe<T>(
|
||||
promise: Promise<T> | (() => T),
|
||||
): Promise<[T | null, Error | null]> {
|
||||
try {
|
||||
const result = promise instanceof Promise ? await promise : promise()
|
||||
return [result, null]
|
||||
}
|
||||
catch (err) {
|
||||
return [null, err instanceof Error ? err : new Error(String(err))]
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue