feat: 新增鉴权
This commit is contained in:
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user