feat(auth): implement login API integration with backend

This commit is contained in:
Quicy
2026-01-29 14:59:21 +08:00
parent 8c7d578657
commit bc63e85d13
28 changed files with 834 additions and 86 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# PostgreSQL database connection URL # PostgreSQL database connection URL
# Format: postgresql://username:password@host:port/database # Format: postgresql://username:password@host:port/database
# Example: postgresql://postgres:password@localhost:5432/memohome # Example: postgresql://postgres:password@localhost:5432/memohome
DATABASE_URL=postgresql://username:password@localhost:5432/database_name DATABASE_URL=postgresql://postgres:1234@localhost:5432/database_name
# ================================== # ==================================
+119
View File
@@ -0,0 +1,119 @@
import { db } from '@memoh/db'
import { users, settings } from '@memoh/db/schema'
import { eq } from 'drizzle-orm'
/**
* 验证用户凭据
* 优先检查是否为 ROOT 用户,否则查询数据库
*/
export const validateUser = async (username: string, password: string) => {
// 检查是否为 ROOT 用户
const rootUser = process.env.ROOT_USER
const rootPassword = process.env.ROOT_USER_PASSWORD
let userId: string | null = null
if (rootUser && rootPassword && username === rootUser) {
if (password === rootPassword) {
// 检查 root 用户是否存在于数据库中
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.username, rootUser))
userId = existingUser?.id
if (!existingUser) {
// 为 root 用户创建数据库记录
// 使用占位符密码哈希,因为实际密码在环境变量中
const [newUser] = await db
.insert(users)
.values({
username: rootUser,
passwordHash: 'ENV_BASED_AUTH', // 占位符,实际使用环境变量验证
role: 'admin',
displayName: 'Root User',
email: null,
avatarUrl: null,
isActive: true,
})
.onConflictDoNothing() // 避免并发创建导致的冲突
.returning({
id: users.id,
})
userId = newUser.id
}
// 检查 root 用户的 settings 是否存在,不存在则创建
const [existingSettings] = await db
.select()
.from(settings)
.where(eq(settings.userId, userId))
if (!existingSettings) {
// 为 root 用户创建默认 settings
await db
.insert(settings)
.values({
userId: userId,
defaultChatModel: null,
defaultEmbeddingModel: null,
defaultSummaryModel: null,
maxContextLoadTime: 60,
language: 'Same as user input',
})
.onConflictDoNothing() // 避免并发创建导致的冲突
}
// 返回 ROOT 用户信息
return {
id: userId,
username: rootUser,
role: 'admin' as const,
displayName: 'Root User',
}
}
return null
}
// 查询数据库中的用户(使用 username 而不是 id
const [user] = await db
.select()
.from(users)
.where(eq(users.username, username))
if (!user) {
return null
}
// 验证密码 (这里使用简单的 Bun.password.verify)
const isValid = await Bun.password.verify(password, user.passwordHash)
if (!isValid) {
return null
}
// 检查账户是否激活
if (!user.isActive) {
return null
}
// 更新最后登录时间
await db
.update(users)
.set({
lastLoginAt: new Date(),
})
.where(eq(users.id, user.id))
return {
id: user.id,
username: user.username,
role: user.role,
displayName: user.displayName || user.username,
email: user.email,
}
}
+182
View File
@@ -0,0 +1,182 @@
import Elysia from 'elysia'
import { adminMiddleware } from '../../middlewares'
import {
GetUserByIdModel,
CreateUserModel,
UpdateUserModel,
DeleteUserModel,
UpdatePasswordModel,
} from './model'
import {
getUsers,
getUserById,
createUser,
updateUser,
deleteUser,
updateUserPassword,
} from './service'
export const userModule = new Elysia({
prefix: '/user',
})
// 使用管理员中间件保护所有路由
.use(adminMiddleware)
// Get all users
.get('/', async ({ query }) => {
try {
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 10
const sortBy = query.sortBy as string || 'createdAt'
const sortOrder = (query.sortOrder as string) || 'desc'
const result = await getUsers({
page,
limit,
sortBy,
sortOrder: sortOrder as 'asc' | 'desc',
})
return {
success: true,
...result,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch users',
}
}
})
// Get user by ID
.get('/:id', async ({ params, set }) => {
try {
const { id } = params
const user = await getUserById(id)
if (!user) {
set.status = 404
return {
success: false,
error: 'User not found',
}
}
return {
success: true,
data: user,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch user',
}
}
}, GetUserByIdModel)
// Create new user
.post('/', async ({ body, set }) => {
try {
const newUser = await createUser(body)
set.status = 201
return {
success: true,
data: newUser,
}
} catch (error) {
if (error instanceof Error && (
error.message.includes('already exists')
)) {
set.status = 409
} else {
set.status = 500
}
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create user',
}
}
}, CreateUserModel)
// Update user
.put('/:id', async ({ params, body, set }) => {
try {
const { id } = params
const updatedUser = await updateUser(id, body)
if (!updatedUser) {
set.status = 404
return {
success: false,
error: 'User not found',
}
}
return {
success: true,
data: updatedUser,
}
} catch (error) {
if (error instanceof Error && error.message.includes('already exists')) {
set.status = 409
} else {
set.status = 500
}
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update user',
}
}
}, UpdateUserModel)
// Delete user
.delete('/:id', async ({ params, set }) => {
try {
const { id } = params
const deletedUser = await deleteUser(id)
if (!deletedUser) {
set.status = 404
return {
success: false,
error: 'User not found',
}
}
return {
success: true,
data: deletedUser,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete user',
}
}
}, DeleteUserModel)
// Update user password
.patch('/:id/password', async ({ params, body, set }) => {
try {
const { id } = params
const updatedUser = await updateUserPassword(id, body.password)
if (!updatedUser) {
set.status = 404
return {
success: false,
error: 'User not found',
}
}
return {
success: true,
data: updatedUser,
message: 'Password updated successfully',
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update password',
}
}
}, UpdatePasswordModel)
+6 -2
View File
@@ -2,9 +2,13 @@ export interface robot{
description: string description: string
time: Date, time: Date,
id: string | number, id: string | number,
type: string type: string,
action:'robot'
} }
export interface user{ export interface user{
description: string, time: Date, id: number | string description: string,
time: Date,
id: number | string,
action:'user'
} }
+4 -1
View File
@@ -20,6 +20,7 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^14.1.0", "@vueuse/core": "^14.1.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -27,7 +28,9 @@
"reka-ui": "^2.7.0", "reka-ui": "^2.7.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vue-sonner": "^2.0.9" "vee-validate": "^4.15.1",
"vue-sonner": "^2.0.9",
"zod": "3.25.76"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "^3.5.26" "vue": "^3.5.26"
@@ -0,0 +1,17 @@
<script lang="ts" setup>
import { Slot } from "reka-ui"
import { useFormField } from "./useFormField"
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
</script>
<template>
<Slot
:id="formItemId"
data-slot="form-control"
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
:aria-invalid="!!error"
>
<slot />
</Slot>
</template>
@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { cn } from '#/lib/utils'
import { useFormField } from "./useFormField"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { formDescriptionId } = useFormField()
</script>
<template>
<p
:id="formDescriptionId"
data-slot="form-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>
@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { useId } from "reka-ui"
import { provide } from "vue"
import { cn } from '#/lib/utils'
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const id = useId()
provide(FORM_ITEM_INJECTION_KEY, id)
</script>
<template>
<div
data-slot="form-item"
:class="cn('grid gap-2', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,25 @@
<script lang="ts" setup>
import type { LabelProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { cn } from '#/lib/utils'
import { Label } from '#/components/label'
import { useFormField } from "./useFormField"
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
const { error, formItemId } = useFormField()
</script>
<template>
<Label
data-slot="form-label"
:data-error="!!error"
:class="cn(
'data-[error=true]:text-destructive',
props.class,
)"
:for="formItemId"
>
<slot />
</Label>
</template>
@@ -0,0 +1,23 @@
<script lang="ts" setup>
import type { HTMLAttributes } from "vue"
import { ErrorMessage } from "vee-validate"
import { toValue } from "vue"
import { cn } from '#/lib/utils'
import { useFormField } from "./useFormField"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { name, formMessageId } = useFormField()
</script>
<template>
<ErrorMessage
:id="formMessageId"
data-slot="form-message"
as="p"
:name="toValue(name)"
:class="cn('text-destructive text-sm', props.class)"
/>
</template>
+7
View File
@@ -0,0 +1,7 @@
export { default as FormControl } from "./FormControl.vue"
export { default as FormDescription } from "./FormDescription.vue"
export { default as FormItem } from "./FormItem.vue"
export { default as FormLabel } from "./FormLabel.vue"
export { default as FormMessage } from "./FormMessage.vue"
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
export { Form, Field as FormField, FieldArray as FormFieldArray } from "vee-validate"
@@ -0,0 +1,4 @@
import type { InjectionKey } from "vue"
export const FORM_ITEM_INJECTION_KEY
= Symbol() as InjectionKey<string>
@@ -0,0 +1,30 @@
import { FieldContextKey } from "vee-validate"
import { computed, inject } from "vue"
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
export function useFormField() {
const fieldContext = inject(FieldContextKey)
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)
if (!fieldContext)
throw new Error("useFormField should be used within <FormField>")
const { name, errorMessage: error, meta } = fieldContext
const id = fieldItemContext
const fieldState = {
valid: computed(() => meta.valid),
isDirty: computed(() => meta.dirty),
isTouched: computed(() => meta.touched),
error,
}
return {
id,
name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
+7 -9
View File
@@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LabelProps } from 'reka-ui' import type { LabelProps } from "reka-ui"
import type { HTMLAttributes } from 'vue' import type { HTMLAttributes } from "vue"
import { reactiveOmit } from '@vueuse/core' import { reactiveOmit } from "@vueuse/core"
import { Label } from 'reka-ui' import { Label } from "reka-ui"
import { cn } from '#/lib/utils' import { cn } from '#/lib/utils'
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>() const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, 'class') const delegatedProps = reactiveOmit(props, "class")
</script> </script>
<template> <template>
@@ -16,9 +16,7 @@ const delegatedProps = reactiveOmit(props, 'class')
v-bind="delegatedProps" v-bind="delegatedProps"
:class=" :class="
cn( cn(
'flex items-center gap-2 text-sm leading-none font-medium select-none text-gray-900 dark:text-gray-100', 'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
'group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50',
'peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
props.class, props.class,
) )
" "
+1 -1
View File
@@ -1 +1 @@
export { default as Label } from './Label.vue' export { default as Label } from "./Label.vue"
+1
View File
@@ -11,6 +11,7 @@ export * from './components/combobox/index'
export * from './components/context-menu/index' export * from './components/context-menu/index'
export * from './components/dialog/index' export * from './components/dialog/index'
export * from './components/dropdown-menu/index' export * from './components/dropdown-menu/index'
export * from './components/form/index'
export * from './components/input/index' export * from './components/input/index'
export * from './components/input-group/index' export * from './components/input-group/index'
export * from './components/kbd/index' export * from './components/kbd/index'
+7 -1
View File
@@ -13,14 +13,20 @@
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@memoh/shared": "workspace:*", "@memoh/shared": "workspace:*",
"@memoh/ui": "workspace:*", "@memoh/ui": "workspace:*",
"@pinia/colada": "^0.21.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@vee-validate/zod": "^4.15.1",
"axios": "^1.13.2",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"vee-validate": "^4.15.1",
"vue": "^3.5.24", "vue": "^3.5.24",
"vue-i18n": "^11.2.8", "vue-i18n": "^11.2.8",
"vue-router": "^4.6.4" "vue-router": "^4.6.4",
"zod": "^4.3.5"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
+23 -4
View File
@@ -49,7 +49,18 @@
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem class="flex justify-center">
<Button
class="flex-[0.7] mb-10"
@click="exit"
>
退出登录
</Button>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarRail /> <SidebarRail />
</Sidebar> </Sidebar>
</aside> </aside>
@@ -67,13 +78,15 @@ import {
SidebarMenuItem, SidebarMenuItem,
SidebarRail, SidebarRail,
CollapsibleTrigger, CollapsibleTrigger,
Collapsible Collapsible,
Button
} from '@memoh/ui' } from '@memoh/ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import SvgIcon from '@jamescoyle/vue-icon' import SvgIcon from '@jamescoyle/vue-icon'
import { mdiRobot, mdiChatOutline, mdiCogBox, mdiListBox, mdiHome, mdiBookArrowDown } from '@mdi/js' import { mdiRobot, mdiChatOutline, mdiCogBox, mdiListBox, mdiHome, mdiBookArrowDown } from '@mdi/js'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import {useUserStore} from '@/store/User.ts'
const router = useRouter() const router = useRouter()
@@ -102,5 +115,11 @@ const sidebarInfo = reactive([{
title: '平台', title: '平台',
name: 'platform', name: 'platform',
icon: mdiBookArrowDown icon: mdiBookArrowDown
}]) }])
const {exitLogin}=useUserStore()
const exit = () => {
exitLogin()
router.replace({name:'Login'})
}
</script> </script>
+5 -1
View File
@@ -1,12 +1,16 @@
import { createApp } from 'vue' import { createApp } from 'vue'
// @ts-ignore
import './style.css' import './style.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import i18n from './i18n' import i18n from './i18n'
import { PiniaColada } from '@pinia/colada'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
createApp(App) createApp(App)
.use(createPinia()) .use(createPinia().use(piniaPluginPersistedstate))
.use(PiniaColada)
.use(router) .use(router)
.use(i18n) .use(i18n)
.mount('#app') .mount('#app')
+124 -58
View File
@@ -1,8 +1,9 @@
<template> <template>
<section <section class="w-screen h-screen flex *:m-auto bg-linear-to-t from-[#BFA4A0] to-[#7784AC] ">
class="w-screen h-screen flex *:m-auto bg-linear-to-t from-[#BFA4A0] to-[#7784AC] " <section
> v-if="!loading"
<section class="w-full max-w-sm flex flex-col gap-10 "> class="w-full max-w-sm flex flex-col gap-10 "
>
<section> <section>
<img <img
src="../../../public/logo.png" src="../../../public/logo.png"
@@ -14,32 +15,51 @@
Memoh Memoh
</h3> </h3>
</section> </section>
<form @submit="login">
<Card class="py-14"> <Card class="py-14">
<CardContent> <CardContent class="flex flex-col [&_input]:py-5">
<form> <FormField
<div class="grid w-full items-center gap-4 [&_input]:py-5"> v-slot="{ componentField }"
<div class="flex flex-col space-y-1.5 gap-2"> name="username"
<Label >
for="email" <FormItem>
class="" <FormLabel class="mb-2">
>Email</Label> Username
<Input </FormLabel>
id="email" <FormControl>
type="email" <Input
placeholder="m@example.com" type="text"
/> placeholder="请输入用户名"
</div> v-bind="componentField"
<div class="flex flex-col space-y-1.5 gap-2"> autocomplete="username"
<div class="flex items-center "> />
<Label for="password">Password</Label> </FormControl>
</div> <blockquote class="h-5">
<Input <FormMessage />
id="password" </blockquote>
type="password" </FormItem>
/> </FormField>
</div> <FormField
</div> v-slot="{ componentField }"
name="password"
>
<FormItem>
<FormLabel class="mb-2">
Password
</FormLabel>
<FormControl>
<Input
type="password"
placeholder="请输入密码"
autocomplete="password"
v-bind="componentField"
/>
</FormControl>
<blockquote class="h-5">
<FormMessage />
</blockquote>
</FormItem>
</FormField>
<div class="flex"> <div class="flex">
<a <a
href="#" href="#"
@@ -48,23 +68,35 @@
Forgot your password? Forgot your password?
</a> </a>
</div> </div>
</form> </CardContent>
</CardContent> <CardFooter class="flex flex-col gap-4">
<CardFooter class="flex flex-col gap-4"> <Button
<Button class="w-full"
class="w-full" type="submit"
@click="login" @click="login"
> >
登录 登录
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
class="w-full" class="w-full"
> >
注册 注册
</Button> </Button>
</CardFooter> </CardFooter>
</Card> </Card>
</form>
</section>
<section
v-else
class="fixed inset-0 flex"
>
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0xMiwxQTExLDExLDAsMSwwLDIzLDEyLDExLDExLDAsMCwwLDEyLDFabTAsMTlhOCw4LDAsMSwxLDgtOEE4LDgsMCwwLDEsMTIsMjBaIiBvcGFjaXR5PSIwLjI1Ii8+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMTAuMTQsMS4xNmExMSwxMSwwLDAsMC05LDguOTJBMS41OSwxLjU5LDAsMCwwLDIuNDYsMTIsMS41MiwxLjUyLDAsMCwwLDQuMTEsMTAuN2E4LDgsMCwwLDEsNi42Ni02LjYxQTEuNDIsMS40MiwwLDAsMCwxMiwyLjY5aDBBMS41NywxLjU3LDAsMCwwLDEwLjE0LDEuMTZaIj48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIGR1cj0iMC43NXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB0eXBlPSJyb3RhdGUiIHZhbHVlcz0iMCAxMiAxMjszNjAgMTIgMTIiLz48L3BhdGg+PC9zdmc+"
alt=""
width="80"
class="m-auto"
>
</section> </section>
</section> </section>
</template> </template>
@@ -75,23 +107,57 @@ import {
CardContent, CardContent,
CardFooter, CardFooter,
Input, Input,
Label,
Button Button,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@memoh/ui' } from '@memoh/ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { toTypedSchema } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import * as z from 'zod'
import request from '@/utils/request'
import { useUserStore } from '@/store/user'
import { ref } from 'vue'
const router = useRouter() const router = useRouter()
const formSchema = toTypedSchema(z.object({
username: z.string().min(1),
password: z.string().min(1),
}))
const form = useForm({
validationSchema: formSchema,
})
const login = async () => { const { login: LoginHandle } = useUserStore()
// 先模拟一下数据 const loading=ref(false)
localStorage.setItem('token','afewfewf') const login = form.handleSubmit(async (values) => {
await router.push('/main') try {
console.log('登录') loading.value=true
} const loginState = await request({
url: '/auth/login',
method: 'post',
data: { ...values }
},false)
const data = loginState?.data?.data
if (data?.token && data?.user) {
LoginHandle(data.user, data.token)
}
router.replace({
name:'Main'
})
} catch (error) {
return error
} finally {
loading.value=false
}
})
</script> </script>
+1 -1
View File
@@ -75,7 +75,7 @@ router.beforeEach((to) => {
if (to.fullPath !== '/login') { if (to.fullPath !== '/login') {
return token ? true : { name: 'Login' } return token ? true : { name: 'Login' }
} else { } else {
return token ? { name: 'Main' } : true return token ? { path:'Main' } : true
} }
}) })
+40
View File
@@ -0,0 +1,40 @@
import { defineStore } from 'pinia'
import { reactive } from 'vue'
type user={
'id': string,
'username': string,
'role': string,
'displayName': string
}
export const useUserStore = defineStore('user', () => {
const userInfo = reactive<user>({
'id': '',
'username': '',
'role': '',
'displayName': ''
})
const login = (userData: user,token:string) => {
localStorage.setItem('token',token)
for (const key of Object.keys(userData) as (keyof user)[]) {
userInfo[key] = userData[key]
}
}
const exitLogin = () => {
localStorage.removeItem('token')
for (const key of Object.keys(userInfo) as (keyof user)[]) {
userInfo[key]=''
}
}
return {
userInfo,
login,
exitLogin
}
}, {
persist:true
})
View File
+29
View File
@@ -0,0 +1,29 @@
import axios, { type AxiosRequestConfig } from 'axios'
import { useRouter } from 'vue-router'
const router=useRouter()
export default (function () {
const axiosInstance = axios.create({
baseURL:'http://localhost:7002/'
})
axiosInstance.interceptors.request.use((config) => {
return config
}, (error) => Promise.reject(error))
axiosInstance.interceptors.response.use((response) => {
return response
}, (error) => {
if (error?.status === 401) {
router.replace({
name:'Login'
})
}
return Promise.reject(error)
})
return (params: AxiosRequestConfig,isToken=true) => {
return axiosInstance(params)
}
}())
+7 -2
View File
@@ -10,7 +10,12 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": true, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true,
"typeRoots": ["./node_modules/@types", "./type.d.ts"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "type.d.ts"],
} }
+2 -1
View File
@@ -3,5 +3,6 @@
"references": [ "references": [
{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" } { "path": "./tsconfig.node.json" }
] ],
} }
+1
View File
@@ -0,0 +1 @@
declare module '@jamescoyle/vue-icon'
+124 -4
View File
@@ -38,6 +38,16 @@ importers:
version: 10.2.0(eslint@9.39.2(jiti@2.6.1)) version: 10.2.0(eslint@9.39.2(jiti@2.6.1))
agent: agent:
docs:
devDependencies:
vitepress:
specifier: ^1.6.0
version: 1.6.4(@algolia/client-search@5.46.2)(@types/node@25.0.3)(axios@1.13.2)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3)
vue:
specifier: ^3.5.0
version: 3.5.26(typescript@5.9.3)
packages/agent:
dependencies: dependencies:
'@ai-sdk/anthropic': '@ai-sdk/anthropic':
specifier: ^3.0.9 specifier: ^3.0.9
@@ -133,6 +143,9 @@ importers:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
'@vee-validate/zod':
specifier: ^4.15.1
version: 4.15.1(vue@3.5.26(typescript@5.9.3))(zod@3.25.76)
'@vueuse/core': '@vueuse/core':
specifier: ^14.1.0 specifier: ^14.1.0
version: 14.1.0(vue@3.5.26(typescript@5.9.3)) version: 14.1.0(vue@3.5.26(typescript@5.9.3))
@@ -154,12 +167,18 @@ importers:
tailwindcss: tailwindcss:
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18 version: 4.1.18
vee-validate:
specifier: ^4.15.1
version: 4.15.1(vue@3.5.26(typescript@5.9.3))
vue: vue:
specifier: ^3.5.26 specifier: ^3.5.26
version: 3.5.26(typescript@5.9.3) version: 3.5.26(typescript@5.9.3)
vue-sonner: vue-sonner:
specifier: ^2.0.9 specifier: ^2.0.9
version: 2.0.9 version: 2.0.9
zod:
specifier: 3.25.76
version: 3.25.76
devDependencies: devDependencies:
'@microsoft/api-extractor': '@microsoft/api-extractor':
specifier: ^7.55.2 specifier: ^7.55.2
@@ -218,21 +237,36 @@ importers:
'@memoh/ui': '@memoh/ui':
specifier: workspace:* specifier: workspace:*
version: link:../ui version: link:../ui
'@pinia/colada':
specifier: ^0.21.1
version: 0.21.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
'@vee-validate/zod':
specifier: ^4.15.1
version: 4.15.1(vue@3.5.26(typescript@5.9.3))(zod@4.3.5)
axios:
specifier: ^1.13.2
version: 1.13.2
dotenv: dotenv:
specifier: ^17.2.3 specifier: ^17.2.3
version: 17.2.3 version: 17.2.3
pinia: pinia:
specifier: ^3.0.4 specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
pinia-plugin-persistedstate:
specifier: ^4.7.1
version: 4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))
tailwindcss: tailwindcss:
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18 version: 4.1.18
tw-animate-css: tw-animate-css:
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
vee-validate:
specifier: ^4.15.1
version: 4.15.1(vue@3.5.26(typescript@5.9.3))
vue: vue:
specifier: ^3.5.24 specifier: ^3.5.24
version: 3.5.26(typescript@5.9.3) version: 3.5.26(typescript@5.9.3)
@@ -242,6 +276,9 @@ importers:
vue-router: vue-router:
specifier: ^4.6.4 specifier: ^4.6.4
version: 4.6.4(vue@3.5.26(typescript@5.9.3)) version: 4.6.4(vue@3.5.26(typescript@5.9.3))
zod:
specifier: ^4.3.5
version: 4.3.5
devDependencies: devDependencies:
'@types/node': '@types/node':
specifier: ^24.10.1 specifier: ^24.10.1
@@ -1465,6 +1502,16 @@ packages:
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'} engines: {node: '>=8.0.0'}
'@pinia/colada@0.21.1':
resolution: {integrity: sha512-VU/XBJ6ayFB2hDltE2hM/T+3gy0i/5tiej4VQLFjOic0svVjmQb7m9re5MEd9ixfvM6s8+MCexVNe9x5bgjbZg==}
peerDependencies:
pinia: ^2.2.6 || ^3.0.0
vue: ^3.5.17
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@polka/url@1.0.0-next.29': '@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
@@ -1871,6 +1918,11 @@ packages:
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
'@vee-validate/zod@4.15.1':
resolution: {integrity: sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==}
peerDependencies:
zod: ^3.24.0
'@vercel/oidc@3.1.0': '@vercel/oidc@3.1.0':
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
@@ -2168,6 +2220,9 @@ packages:
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.13.2:
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
axios@1.7.7: axios@1.7.7:
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
@@ -3269,6 +3324,20 @@ packages:
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
hasBin: true hasBin: true
pinia-plugin-persistedstate@4.7.1:
resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
'@pinia/nuxt': '>=0.10.0'
pinia: '>=3.0.0'
peerDependenciesMeta:
'@nuxt/kit':
optional: true
'@pinia/nuxt':
optional: true
pinia:
optional: true
pinia@3.0.4: pinia@3.0.4:
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
peerDependencies: peerDependencies:
@@ -3626,6 +3695,9 @@ packages:
type-is@2.0.1: type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript-eslint@8.52.0: typescript-eslint@8.52.0:
resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==} resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==}
@@ -3733,6 +3805,10 @@ packages:
vary@1.1.2: vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
vee-validate@4.15.1:
resolution: {integrity: sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==}
peerDependencies:
vue: ^3.4.26
vfile-message@4.0.3: vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
@@ -5077,6 +5153,14 @@ snapshots:
'@opentelemetry/api@1.9.0': {} '@opentelemetry/api@1.9.0': {}
'@pinia/colada@0.21.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))':
dependencies:
pinia: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
vue: 3.5.26(typescript@5.9.3)
'@pkgjs/parseargs@0.11.0':
optional: true
'@polka/url@1.0.0-next.29': {} '@polka/url@1.0.0-next.29': {}
'@rolldown/pluginutils@1.0.0-beta.53': {} '@rolldown/pluginutils@1.0.0-beta.53': {}
@@ -5471,6 +5555,22 @@ snapshots:
'@ungap/structured-clone@1.3.0': {} '@ungap/structured-clone@1.3.0': {}
'@vee-validate/zod@4.15.1(vue@3.5.26(typescript@5.9.3))(zod@3.25.76)':
dependencies:
type-fest: 4.41.0
vee-validate: 4.15.1(vue@3.5.26(typescript@5.9.3))
zod: 3.25.76
transitivePeerDependencies:
- vue
'@vee-validate/zod@4.15.1(vue@3.5.26(typescript@5.9.3))(zod@4.3.5)':
dependencies:
type-fest: 4.41.0
vee-validate: 4.15.1(vue@3.5.26(typescript@5.9.3))
zod: 4.3.5
transitivePeerDependencies:
- vue
'@vercel/oidc@3.1.0': {} '@vercel/oidc@3.1.0': {}
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.0.3)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))': '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.0.3)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))':
@@ -5695,13 +5795,13 @@ snapshots:
'@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3)) '@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3))
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
'@vueuse/integrations@12.8.2(axios@1.7.7)(focus-trap@7.8.0)(typescript@5.9.3)': '@vueuse/integrations@12.8.2(axios@1.13.2)(focus-trap@7.8.0)(typescript@5.9.3)':
dependencies: dependencies:
'@vueuse/core': 12.8.2(typescript@5.9.3) '@vueuse/core': 12.8.2(typescript@5.9.3)
'@vueuse/shared': 12.8.2(typescript@5.9.3) '@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
optionalDependencies: optionalDependencies:
axios: 1.7.7 axios: 1.13.2
focus-trap: 7.8.0 focus-trap: 7.8.0
transitivePeerDependencies: transitivePeerDependencies:
- typescript - typescript
@@ -5827,6 +5927,14 @@ snapshots:
asynckit@0.4.0: asynckit@0.4.0:
optional: true optional: true
axios@1.13.2:
dependencies:
follow-redirects: 1.15.11
form-data: 4.0.5
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
axios@1.7.7: axios@1.7.7:
dependencies: dependencies:
follow-redirects: 1.15.11 follow-redirects: 1.15.11
@@ -6941,6 +7049,12 @@ snapshots:
pidtree@0.6.0: {} pidtree@0.6.0: {}
pinia-plugin-persistedstate@4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))):
dependencies:
defu: 6.1.4
optionalDependencies:
pinia: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)):
dependencies: dependencies:
'@vue/devtools-api': 7.7.9 '@vue/devtools-api': 7.7.9
@@ -7338,6 +7452,7 @@ snapshots:
content-type: 1.0.5 content-type: 1.0.5
media-typer: 1.1.0 media-typer: 1.1.0
mime-types: 3.0.2 mime-types: 3.0.2
type-fest@4.41.0: {}
typescript-eslint@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): typescript-eslint@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies: dependencies:
@@ -7434,6 +7549,11 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vary@1.1.2: {} vary@1.1.2: {}
vee-validate@4.15.1(vue@3.5.26(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 7.7.9
type-fest: 4.41.0
vue: 3.5.26(typescript@5.9.3)
vfile-message@4.0.3: vfile-message@4.0.3:
dependencies: dependencies:
@@ -7539,7 +7659,7 @@ snapshots:
lightningcss: 1.30.2 lightningcss: 1.30.2
tsx: 4.21.0 tsx: 4.21.0
vitepress@1.6.4(@algolia/client-search@5.46.2)(@types/node@25.0.3)(axios@1.7.7)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3): vitepress@1.6.4(@algolia/client-search@5.46.2)(@types/node@25.0.3)(axios@1.13.2)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3):
dependencies: dependencies:
'@docsearch/css': 3.8.2 '@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.46.2)(search-insights@2.17.3) '@docsearch/js': 3.8.2(@algolia/client-search@5.46.2)(search-insights@2.17.3)
@@ -7552,7 +7672,7 @@ snapshots:
'@vue/devtools-api': 7.7.9 '@vue/devtools-api': 7.7.9
'@vue/shared': 3.5.26 '@vue/shared': 3.5.26
'@vueuse/core': 12.8.2(typescript@5.9.3) '@vueuse/core': 12.8.2(typescript@5.9.3)
'@vueuse/integrations': 12.8.2(axios@1.7.7)(focus-trap@7.8.0)(typescript@5.9.3) '@vueuse/integrations': 12.8.2(axios@1.13.2)(focus-trap@7.8.0)(typescript@5.9.3)
focus-trap: 7.8.0 focus-trap: 7.8.0
mark.js: 8.11.1 mark.js: 8.11.1
minisearch: 7.2.0 minisearch: 7.2.0