mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat(auth): implement login API integration with backend
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
# PostgreSQL database connection URL
|
||||
# Format: postgresql://username:password@host:port/database
|
||||
# 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
|
||||
|
||||
|
||||
# ==================================
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2,9 +2,13 @@ export interface robot{
|
||||
description: string
|
||||
time: Date,
|
||||
id: string | number,
|
||||
type: string
|
||||
type: string,
|
||||
action:'robot'
|
||||
}
|
||||
|
||||
export interface user{
|
||||
description: string, time: Date, id: number | string
|
||||
description: string,
|
||||
time: Date,
|
||||
id: number | string,
|
||||
action:'user'
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -27,7 +28,9 @@
|
||||
"reka-ui": "^2.7.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue-sonner": "^2.0.9"
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { LabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Label } from 'reka-ui'
|
||||
import type { LabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Label } from "reka-ui"
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -16,9 +16,7 @@ const delegatedProps = reactiveOmit(props, 'class')
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none text-gray-900 dark:text-gray-100',
|
||||
'group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50',
|
||||
'peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
'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',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as Label } from './Label.vue'
|
||||
export { default as Label } from "./Label.vue"
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from './components/combobox/index'
|
||||
export * from './components/context-menu/index'
|
||||
export * from './components/dialog/index'
|
||||
export * from './components/dropdown-menu/index'
|
||||
export * from './components/form/index'
|
||||
export * from './components/input/index'
|
||||
export * from './components/input-group/index'
|
||||
export * from './components/kbd/index'
|
||||
|
||||
@@ -13,14 +13,20 @@
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@memoh/shared": "workspace:*",
|
||||
"@memoh/ui": "workspace:*",
|
||||
"@pinia/colada": "^0.21.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
"vue-router": "^4.6.4",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
|
||||
@@ -49,7 +49,18 @@
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem class="flex justify-center">
|
||||
<Button
|
||||
class="flex-[0.7] mb-10"
|
||||
@click="exit"
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</aside>
|
||||
@@ -67,13 +78,15 @@ import {
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
CollapsibleTrigger,
|
||||
Collapsible
|
||||
|
||||
Collapsible,
|
||||
Button
|
||||
} from '@memoh/ui'
|
||||
import { reactive } from 'vue'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobot, mdiChatOutline, mdiCogBox, mdiListBox, mdiHome, mdiBookArrowDown } from '@mdi/js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {useUserStore} from '@/store/User.ts'
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -102,5 +115,11 @@ const sidebarInfo = reactive([{
|
||||
title: '平台',
|
||||
name: 'platform',
|
||||
icon: mdiBookArrowDown
|
||||
}])
|
||||
}])
|
||||
|
||||
const {exitLogin}=useUserStore()
|
||||
const exit = () => {
|
||||
exitLogin()
|
||||
router.replace({name:'Login'})
|
||||
}
|
||||
</script>
|
||||
@@ -1,12 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
// @ts-ignore
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
import i18n from './i18n'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.use(createPinia().use(piniaPluginPersistedstate))
|
||||
.use(PiniaColada)
|
||||
.use(router)
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<section
|
||||
class="w-screen h-screen flex *:m-auto bg-linear-to-t from-[#BFA4A0] to-[#7784AC] "
|
||||
>
|
||||
<section class="w-full max-w-sm flex flex-col gap-10 ">
|
||||
<section class="w-screen h-screen flex *:m-auto bg-linear-to-t from-[#BFA4A0] to-[#7784AC] ">
|
||||
<section
|
||||
v-if="!loading"
|
||||
class="w-full max-w-sm flex flex-col gap-10 "
|
||||
>
|
||||
<section>
|
||||
<img
|
||||
src="../../../public/logo.png"
|
||||
@@ -14,32 +15,51 @@
|
||||
Memoh
|
||||
</h3>
|
||||
</section>
|
||||
|
||||
<Card class="py-14">
|
||||
<CardContent>
|
||||
<form>
|
||||
<div class="grid w-full items-center gap-4 [&_input]:py-5">
|
||||
<div class="flex flex-col space-y-1.5 gap-2">
|
||||
<Label
|
||||
for="email"
|
||||
class=""
|
||||
>Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-1.5 gap-2">
|
||||
<div class="flex items-center ">
|
||||
<Label for="password">Password</Label>
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<form @submit="login">
|
||||
<Card class="py-14">
|
||||
<CardContent class="flex flex-col [&_input]:py-5">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="username"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Username
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
v-bind="componentField"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
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">
|
||||
<a
|
||||
href="#"
|
||||
@@ -48,23 +68,35 @@
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter class="flex flex-col gap-4">
|
||||
<Button
|
||||
class="w-full"
|
||||
@click="login"
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</CardContent>
|
||||
<CardFooter class="flex flex-col gap-4">
|
||||
<Button
|
||||
class="w-full"
|
||||
type="submit"
|
||||
@click="login"
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</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>
|
||||
</template>
|
||||
@@ -75,23 +107,57 @@ import {
|
||||
CardContent,
|
||||
CardFooter,
|
||||
Input,
|
||||
Label,
|
||||
Button
|
||||
|
||||
Button,
|
||||
FormControl,
|
||||
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@memoh/ui'
|
||||
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 formSchema = toTypedSchema(z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
}))
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
})
|
||||
|
||||
const login = async () => {
|
||||
// 先模拟一下数据
|
||||
localStorage.setItem('token','afewfewf')
|
||||
await router.push('/main')
|
||||
console.log('登录')
|
||||
}
|
||||
|
||||
|
||||
const { login: LoginHandle } = useUserStore()
|
||||
const loading=ref(false)
|
||||
const login = form.handleSubmit(async (values) => {
|
||||
try {
|
||||
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>
|
||||
@@ -75,7 +75,7 @@ router.beforeEach((to) => {
|
||||
if (to.fullPath !== '/login') {
|
||||
return token ? true : { name: 'Login' }
|
||||
} else {
|
||||
return token ? { name: 'Main' } : true
|
||||
return token ? { path:'Main' } : true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}())
|
||||
@@ -10,7 +10,12 @@
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": 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"],
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
],
|
||||
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
declare module '@jamescoyle/vue-icon'
|
||||
Generated
+124
-4
@@ -38,6 +38,16 @@ importers:
|
||||
version: 10.2.0(eslint@9.39.2(jiti@2.6.1))
|
||||
|
||||
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:
|
||||
'@ai-sdk/anthropic':
|
||||
specifier: ^3.0.9
|
||||
@@ -133,6 +143,9 @@ importers:
|
||||
'@tailwindcss/vite':
|
||||
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))
|
||||
'@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':
|
||||
specifier: ^14.1.0
|
||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||
@@ -154,12 +167,18 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: ^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:
|
||||
specifier: ^3.5.26
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
vue-sonner:
|
||||
specifier: ^2.0.9
|
||||
version: 2.0.9
|
||||
zod:
|
||||
specifier: 3.25.76
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@microsoft/api-extractor':
|
||||
specifier: ^7.55.2
|
||||
@@ -218,21 +237,36 @@ importers:
|
||||
'@memoh/ui':
|
||||
specifier: workspace:*
|
||||
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':
|
||||
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))
|
||||
'@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:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
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:
|
||||
specifier: ^4.1.18
|
||||
version: 4.1.18
|
||||
tw-animate-css:
|
||||
specifier: ^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:
|
||||
specifier: ^3.5.24
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
@@ -242,6 +276,9 @@ importers:
|
||||
vue-router:
|
||||
specifier: ^4.6.4
|
||||
version: 4.6.4(vue@3.5.26(typescript@5.9.3))
|
||||
zod:
|
||||
specifier: ^4.3.5
|
||||
version: 4.3.5
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^24.10.1
|
||||
@@ -1465,6 +1502,16 @@ packages:
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
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':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
@@ -1871,6 +1918,11 @@ packages:
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
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':
|
||||
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
|
||||
engines: {node: '>= 20'}
|
||||
@@ -2168,6 +2220,9 @@ packages:
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
axios@1.13.2:
|
||||
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
||||
|
||||
axios@1.7.7:
|
||||
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
|
||||
|
||||
@@ -3269,6 +3324,20 @@ packages:
|
||||
engines: {node: '>=0.10'}
|
||||
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:
|
||||
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
|
||||
peerDependencies:
|
||||
@@ -3626,6 +3695,9 @@ packages:
|
||||
type-is@2.0.1:
|
||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typescript-eslint@8.52.0:
|
||||
resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==}
|
||||
@@ -3733,6 +3805,10 @@ packages:
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
vee-validate@4.15.1:
|
||||
resolution: {integrity: sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==}
|
||||
peerDependencies:
|
||||
vue: ^3.4.26
|
||||
|
||||
vfile-message@4.0.3:
|
||||
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
|
||||
@@ -5077,6 +5153,14 @@ snapshots:
|
||||
|
||||
'@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': {}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
||||
@@ -5471,6 +5555,22 @@ snapshots:
|
||||
|
||||
'@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': {}
|
||||
|
||||
'@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))
|
||||
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:
|
||||
'@vueuse/core': 12.8.2(typescript@5.9.3)
|
||||
'@vueuse/shared': 12.8.2(typescript@5.9.3)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
axios: 1.7.7
|
||||
axios: 1.13.2
|
||||
focus-trap: 7.8.0
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
@@ -5827,6 +5927,14 @@ snapshots:
|
||||
asynckit@0.4.0:
|
||||
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:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
@@ -6941,6 +7049,12 @@ snapshots:
|
||||
|
||||
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)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.9
|
||||
@@ -7338,6 +7452,7 @@ snapshots:
|
||||
content-type: 1.0.5
|
||||
media-typer: 1.1.0
|
||||
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):
|
||||
dependencies:
|
||||
@@ -7434,6 +7549,11 @@ snapshots:
|
||||
util-deprecate@1.0.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:
|
||||
dependencies:
|
||||
@@ -7539,7 +7659,7 @@ snapshots:
|
||||
lightningcss: 1.30.2
|
||||
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:
|
||||
'@docsearch/css': 3.8.2
|
||||
'@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/shared': 3.5.26
|
||||
'@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
|
||||
mark.js: 8.11.1
|
||||
minisearch: 7.2.0
|
||||
|
||||
Reference in New Issue
Block a user