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
|
# 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
|
||||||
|
|
||||||
|
|
||||||
# ==================================
|
# ==================================
|
||||||
|
|||||||
@@ -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
|
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'
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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">
|
<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 @@
|
|||||||
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/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'
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
"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"],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@
|
|||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
{ "path": "./tsconfig.node.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))
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user