feat: 新增鉴权

This commit is contained in:
2026-05-22 22:38:01 +08:00
parent b02f15f735
commit 3857f77f8b
38 changed files with 1697 additions and 195 deletions
+54
View File
@@ -1,11 +1,25 @@
<!-- app/pages/dashboard.vue - 运行状态与最近问答记录页面 -->
<script lang="ts" setup>
import { ArrowPathIcon } from "@heroicons/vue/24/outline";
import QaRecordDetail from "@/components/dashboard/QaRecordDetail.vue";
import QaRecordsTable from "@/components/dashboard/QaRecordsTable.vue";
import StatsOverview from "@/components/dashboard/StatsOverview.vue";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "@/components/ui/card";
import { useAnswerStore } from "@/stores/answer";
import { useAuthStore } from "@/stores/auth";
definePageMeta({ middleware: "auth" });
const answerStore = useAnswerStore();
const authStore = useAuthStore();
useHead({
title: "Dashboard - OCS AI 答题服务"
@@ -22,10 +36,50 @@ if (import.meta.client) {
void answerStore.loadDashboard();
});
}
const refreshing = ref(false);
const handleRefreshToken = async () => {
refreshing.value = true;
await authStore.refreshApiToken();
refreshing.value = false;
};
</script>
<template>
<div class="grid gap-6">
<!-- OCS 油猴脚本 API token 配置卡片 -->
<Card v-if="authStore.user?.apiToken">
<CardHeader>
<CardTitle>OCS 脚本 Token</CardTitle>
<CardDescription>
OCS 油猴脚本的服务器配置中填入此
token脚本将以你的账号身份调用答题接口
</CardDescription>
</CardHeader>
<CardContent>
<div class="flex items-center gap-2">
<code
class="flex-1 overflow-x-auto bg-muted px-3 py-2 font-mono text-sm rounded-full"
>
{{ authStore.user.apiToken }}
</code>
<CopyCom :text="authStore.user.apiToken" />
<Button
variant="outline"
size="icon"
:disabled="refreshing"
title="刷新 Token"
@click="handleRefreshToken"
>
<ArrowPathIcon
:class="['size-4', refreshing ? 'animate-spin' : '']"
/>
</Button>
</div>
</CardContent>
</Card>
<StatsOverview />
<QaRecordsTable />
<QaRecordDetail />
+112
View File
@@ -0,0 +1,112 @@
<!-- app/pages/login.vue - 邮箱密码登录页 -->
<script lang="ts" setup>
import { toTypedSchema } from "@vee-validate/zod";
import { useForm } from "vee-validate";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth";
useHead({ title: "登录 - OCS AI 答题服务" });
const authStore = useAuthStore();
// 已登录用户访问登录页,直接跳首页
watchEffect(() => {
if (authStore.isOnline) navigateTo("/");
});
const formSchema = toTypedSchema(
z.object({
email: z
.string({ required_error: "邮箱不能为空" })
.min(1, "邮箱不能为空")
.email("请输入有效的邮箱地址"),
password: z.string({ required_error: "密码不能为空" }).min(1, "密码不能为空")
})
);
const { handleSubmit, isSubmitting } = useForm({
validationSchema: formSchema
});
const onSubmit = handleSubmit(async (values) => {
const ok = await authStore.signIn(values.email, values.password);
if (ok) {
await navigateTo("/");
}
});
</script>
<template>
<div class="flex min-h-[60vh] items-center justify-center">
<div class="w-full max-w-sm space-y-6">
<div class="space-y-1 text-center">
<h1 class="text-2xl font-semibold">登录</h1>
<p class="text-sm text-muted-foreground">使用邮箱和密码登录账号</p>
</div>
<form class="space-y-4" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input
type="email"
autocomplete="email"
placeholder="your@email.com"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input
type="password"
autocomplete="current-password"
placeholder="••••••••"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<p v-if="authStore.error" class="text-sm text-destructive">
{{ authStore.error }}
</p>
<Button
type="submit"
:disabled="isSubmitting || authStore.loading || !authStore.initialized"
class="w-full"
>
{{ authStore.loading ? "登录中..." : "登录" }}
</Button>
</form>
<p class="text-center text-sm text-muted-foreground">
还没有账号
<NuxtLink
to="/register"
class="underline underline-offset-4 hover:text-primary"
>
注册
</NuxtLink>
</p>
</div>
</div>
</template>
+136
View File
@@ -0,0 +1,136 @@
<!-- app/pages/register.vue - 新用户注册页 -->
<script lang="ts" setup>
import { toTypedSchema } from "@vee-validate/zod";
import { useForm } from "vee-validate";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth";
useHead({ title: "注册 - OCS AI 答题服务" });
const authStore = useAuthStore();
// 已登录用户访问注册页,直接跳首页
watchEffect(() => {
if (authStore.isOnline) navigateTo("/");
});
const formSchema = toTypedSchema(
z.object({
name: z
.string({ required_error: "昵称不能为空" })
.min(1, "昵称不能为空")
.max(20, "昵称不能超过 20 个字符"),
email: z
.string({ required_error: "邮箱不能为空" })
.min(1, "邮箱不能为空")
.email("请输入有效的邮箱地址"),
password: z
.string({ required_error: "密码不能为空" })
.min(8, "密码至少 8 位")
.max(100, "密码不能超过 100 位")
.regex(/[a-zA-Z]/, "密码必须包含英文字母")
.regex(/[0-9]/, "密码必须包含数字")
})
);
const { handleSubmit, isSubmitting } = useForm({
validationSchema: formSchema
});
const onSubmit = handleSubmit(async (values) => {
const ok = await authStore.signUp(values.name, values.email, values.password);
if (ok) {
await navigateTo("/");
}
});
</script>
<template>
<div class="flex min-h-[60vh] items-center justify-center">
<div class="w-full max-w-sm space-y-6">
<div class="space-y-1 text-center">
<h1 class="text-2xl font-semibold">注册</h1>
<p class="text-sm text-muted-foreground">创建新账号</p>
</div>
<form class="space-y-4" @submit="onSubmit">
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>昵称</FormLabel>
<FormControl>
<Input
type="text"
autocomplete="name"
placeholder="你的昵称"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="email">
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input
type="email"
autocomplete="email"
placeholder="your@email.com"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="password">
<FormItem>
<FormLabel>密码</FormLabel>
<FormControl>
<Input
type="password"
autocomplete="new-password"
placeholder="至少 8 位"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<p v-if="authStore.error" class="text-sm text-destructive">
{{ authStore.error }}
</p>
<Button
type="submit"
:disabled="isSubmitting || authStore.loading || !authStore.initialized"
class="w-full"
>
{{ authStore.loading ? "注册中..." : "注册" }}
</Button>
</form>
<p class="text-center text-sm text-muted-foreground">
已有账号
<NuxtLink
to="/login"
class="underline underline-offset-4 hover:text-primary"
>
登录
</NuxtLink>
</p>
</div>
</div>
</template>