first commit

This commit is contained in:
2026-04-07 16:05:05 +08:00
commit 9d9bdbb1ce
136 changed files with 5103 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2021: true
},
extends: [
'plugin:vue/vue3-recommended',
'eslint:recommended',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }]
}
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+7
View File
@@ -0,0 +1,7 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none",
"arrowParens": "avoid"
}
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/vite.svg">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SentClaw - AI 智能助手</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
{
"name": "sentclaw-web",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.5",
"pinia": "^2.1.7",
"axios": "^1.6.2",
"element-plus": "^2.5.0",
"@element-plus/icons-vue": "^2.3.1",
"nprogress": "^0.2.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"@vue/tsconfig": "^0.5.0",
"typescript": "~5.3.0",
"vite": "^5.0.0",
"vue-tsc": "^1.8.27",
"eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.2",
"@typescript-eslint/eslint-plugin": "^6.16.0",
"@typescript-eslint/parser": "^6.16.0",
"prettier": "^3.1.1",
"sass": "^1.69.5"
}
}
+13
View File
@@ -0,0 +1,13 @@
<template>
<router-view />
</template>
<script setup lang="ts">
</script>
<style lang="scss">
#app {
width: 100%;
height: 100vh;
}
</style>
+29
View File
@@ -0,0 +1,29 @@
import request from './request'
import type { Agent } from '../types'
export interface AgentCreateData {
name: string
description?: string
system_prompt?: string
model_id?: string
temperature?: number
max_tokens?: number
}
export interface AgentUpdateData {
name?: string
description?: string
system_prompt?: string
model_id?: string
temperature?: number
max_tokens?: number
is_active?: boolean
}
export const agentApi = {
getAgents: (workspaceId: number) => request.get<Agent[]>(`/workspaces/${workspaceId}/agents`),
createAgent: (workspaceId: number, data: AgentCreateData) => request.post<Agent>(`/workspaces/${workspaceId}/agents`, data),
getAgent: (id: number) => request.get<Agent>(`/agents/${id}`),
updateAgent: (id: number, data: AgentUpdateData) => request.put<Agent>(`/agents/${id}`, data),
deleteAgent: (id: number) => request.delete(`/agents/${id}`)
}
+26
View File
@@ -0,0 +1,26 @@
import request from './request'
import type { User } from '../types'
export interface LoginRequest {
username: string
password: string
}
export interface RegisterRequest {
username: string
password: string
email?: string
}
export interface AuthResponse {
message: string
user: User
access_token: string
}
export const authApi = {
login: (data: LoginRequest) => request.post<AuthResponse>('/auth/login', data),
register: (data: RegisterRequest) => request.post<AuthResponse>('/auth/register', data),
getCurrentUser: () => request.get<User>('/users/me'),
updateUser: (data: Partial<User>) => request.put<User>('/users/me', data)
}
+19
View File
@@ -0,0 +1,19 @@
import request from './request'
import type { Conversation } from '../types'
export interface ConversationCreateData {
agent_id: number
title?: string
}
export interface ConversationUpdateData {
title?: string
status?: string
}
export const conversationApi = {
getConversations: (workspaceId: number) => request.get<Conversation[]>(`/workspaces/${workspaceId}/conversations`),
createConversation: (workspaceId: number, data: ConversationCreateData) => request.post<Conversation>(`/workspaces/${workspaceId}/conversations`, data),
getConversation: (id: number) => request.get<Conversation>(`/conversations/${id}`),
updateConversation: (id: number, data: ConversationUpdateData) => request.put<Conversation>(`/conversations/${id}`, data)
}
+14
View File
@@ -0,0 +1,14 @@
import request from './request'
import type { Message } from '../types'
export interface MessageCreateData {
role: string
content: string
tool_calls?: any[]
metadata?: any
}
export const messageApi = {
getMessages: (conversationId: number) => request.get<Message[]>(`/conversations/${conversationId}/messages`),
createMessage: (conversationId: number, data: MessageCreateData) => request.post<Message>(`/conversations/${conversationId}/messages`, data)
}
+40
View File
@@ -0,0 +1,40 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
const request = axios.create({
baseURL: '/api',
timeout: 30000
})
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
request.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
const message = error.response?.data?.error || error.message || '请求失败'
ElMessage.error(message)
if (error.response?.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
+20
View File
@@ -0,0 +1,20 @@
import request from './request'
import type { Workspace } from '../types'
export interface WorkspaceCreateData {
name: string
description?: string
}
export interface WorkspaceUpdateData {
name?: string
description?: string
}
export const workspaceApi = {
getWorkspaces: () => request.get<Workspace[]>('/workspaces'),
createWorkspace: (data: WorkspaceCreateData) => request.post<Workspace>('/workspaces', data),
getWorkspace: (id: number) => request.get<Workspace>(`/workspaces/${id}`),
updateWorkspace: (id: number, data: WorkspaceUpdateData) => request.put<Workspace>(`/workspaces/${id}`, data),
deleteWorkspace: (id: number) => request.delete(`/workspaces/${id}`)
}
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
+191
View File
@@ -0,0 +1,191 @@
<template>
<div class="main-layout">
<el-container>
<el-aside width="240px">
<div class="logo">
<h1>SentClaw</h1>
</div>
<el-menu
:default-active="activeMenu"
class="sidebar-menu"
router
>
<el-menu-item index="/">
<el-icon><HomeFilled /></el-icon>
<span>首页</span>
</el-menu-item>
<el-menu-item index="/chat">
<el-icon><ChatDotRound /></el-icon>
<span>对话</span>
</el-menu-item>
<el-menu-item index="/agents">
<el-icon><Avatar /></el-icon>
<span>Agent</span>
</el-menu-item>
<el-menu-item index="/settings">
<el-icon><Setting /></el-icon>
<span>设置</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header>
<div class="header-content">
<h2>{{ pageTitle }}</h2>
<div class="user-info">
<el-dropdown @command="handleCommand">
<span class="user-dropdown">
<el-avatar :size="32" :src="user?.avatar" />
<span>{{ user?.username }}</span>
<el-icon><ArrowDown /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="profile">个人资料</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '../stores/user'
import { HomeFilled, ChatDotRound, Avatar, Setting, ArrowDown } from '@element-plus/icons-vue'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const user = computed(() => userStore.user)
const pageTitle = computed(() => {
const titles: Record<string, string> = {
'/': '首页',
'/chat': '对话',
'/agents': 'Agent 管理',
'/settings': '设置'
}
return titles[route.path] || 'SentClaw'
})
const activeMenu = computed(() => route.path)
const handleCommand = (command: string) => {
if (command === 'logout') {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
userStore.logout()
ElMessage.success('已退出登录')
router.push('/login')
}).catch(() => {})
} else if (command === 'profile') {
router.push('/settings')
}
}
</script>
<style lang="scss" scoped>
.main-layout {
height: 100vh;
}
.el-container {
height: 100%;
}
.el-aside {
background-color: #304156;
color: #fff;
}
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background-color: #2b3a4a;
border-bottom: 1px solid #1f2d3d;
h1 {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #fff;
}
}
.sidebar-menu {
border: none;
background-color: #304156;
:deep(.el-menu-item) {
color: #bfcbd9;
&:hover {
background-color: #263445;
}
&.is-active {
background-color: #409eff;
color: #fff;
}
}
}
.el-header {
background-color: #fff;
border-bottom: 1px solid #e6e6e6;
display: flex;
align-items: center;
padding: 0 20px;
}
.header-content {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #303133;
}
}
.user-dropdown {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: #606266;
transition: color 0.3s;
&:hover {
color: #409eff;
}
}
.el-main {
background-color: #f0f2f5;
padding: 20px;
}
</style>
+21
View File
@@ -0,0 +1,21 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import './styles/main.scss'
const app = createApp(App)
const pinia = createPinia()
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
+65
View File
@@ -0,0 +1,65 @@
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
import { useUserStore } from '../stores/user'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: () => import('../layouts/MainLayout.vue'),
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'Home',
component: () => import('../views/Home.vue')
},
{
path: 'chat/:conversationId?',
name: 'Chat',
component: () => import('../views/Chat.vue')
},
{
path: 'agents',
name: 'Agents',
component: () => import('../views/Agents.vue')
},
{
path: 'settings',
name: 'Settings',
component: () => import('../views/Settings.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
const requiresAuth = to.meta.requiresAuth !== false
if (requiresAuth && !userStore.token) {
next('/login')
} else if ((to.path === '/login' || to.path === '/register') && userStore.token) {
next('/')
} else {
next()
}
})
export default router
+54
View File
@@ -0,0 +1,54 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { User } from '../types'
import { authApi } from '../api/auth'
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(localStorage.getItem('token'))
const login = async (username: string, password: string) => {
const response = await authApi.login({ username, password })
user.value = response.user
token.value = response.access_token
localStorage.setItem('token', response.access_token)
localStorage.setItem('user', JSON.stringify(response.user))
return response
}
const register = async (username: string, password: string, email?: string) => {
const response = await authApi.register({ username, password, email })
user.value = response.user
token.value = response.access_token
localStorage.setItem('token', response.access_token)
localStorage.setItem('user', JSON.stringify(response.user))
return response
}
const logout = () => {
user.value = null
token.value = null
localStorage.removeItem('token')
localStorage.removeItem('user')
}
const loadUser = async () => {
if (!token.value) return
try {
const response = await authApi.getCurrentUser()
user.value = response
localStorage.setItem('user', JSON.stringify(response))
} catch (error) {
logout()
}
}
return {
user,
token,
login,
register,
logout,
loadUser
}
})
+58
View File
@@ -0,0 +1,58 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { Workspace } from '../types'
import { workspaceApi } from '../api/workspace'
export const useWorkspaceStore = defineStore('workspace', () => {
const workspaces = ref<Workspace[]>([])
const currentWorkspace = ref<Workspace | null>(null)
const loadWorkspaces = async () => {
const response = await workspaceApi.getWorkspaces()
workspaces.value = response
if (!currentWorkspace.value && workspaces.value.length > 0) {
currentWorkspace.value = workspaces.value[0]
}
return response
}
const createWorkspace = async (name: string, description?: string) => {
const response = await workspaceApi.createWorkspace({ name, description })
workspaces.value.push(response)
return response
}
const updateWorkspace = async (id: number, data: any) => {
const response = await workspaceApi.updateWorkspace(id, data)
const index = workspaces.value.findIndex((w) => w.id === id)
if (index !== -1) {
workspaces.value[index] = response
}
if (currentWorkspace.value?.id === id) {
currentWorkspace.value = response
}
return response
}
const deleteWorkspace = async (id: number) => {
await workspaceApi.deleteWorkspace(id)
workspaces.value = workspaces.value.filter((w) => w.id !== id)
if (currentWorkspace.value?.id === id) {
currentWorkspace.value = workspaces.value[0] || null
}
}
const setCurrentWorkspace = (workspace: Workspace) => {
currentWorkspace.value = workspace
}
return {
workspaces,
currentWorkspace,
loadWorkspaces,
createWorkspace,
updateWorkspace,
deleteWorkspace,
setCurrentWorkspace
}
})
+11
View File
@@ -0,0 +1,11 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
+131
View File
@@ -0,0 +1,131 @@
export interface User {
id: number
username: string
email?: string
avatar?: string
is_active: boolean
is_admin: boolean
created_at: string
updated_at: string
}
export interface Workspace {
id: number
user_id: number
name: string
description?: string
is_default: boolean
created_at: string
updated_at: string
}
export interface Agent {
id: number
workspace_id: number
name: string
description?: string
system_prompt?: string
model_id?: string
temperature: number
max_tokens: number
is_active: boolean
created_at: string
updated_at: string
}
export interface Conversation {
id: number
workspace_id: number
agent_id: number
title?: string
channel: string
channel_user_id?: string
status: string
created_at: string
updated_at: string
}
export interface Message {
id: number
conversation_id: number
role: string
content: string
tokens?: number
model?: string
tool_calls?: any[]
metadata?: any
created_at: string
}
export interface Tool {
id: number
name: string
type: string
description?: string
config?: any
is_active: boolean
created_at: string
updated_at: string
}
export interface Skill {
id: number
name: string
version: string
description?: string
author?: string
repository?: string
config?: any
is_installed: boolean
is_active: boolean
created_at: string
updated_at: string
}
export interface Memory {
id: number
workspace_id: number
agent_id?: number
type: string
content: string
tags?: string[]
importance: number
created_at: string
updated_at: string
}
export interface Model {
id: number
provider: string
name: string
model_id: string
api_key?: string
base_url?: string
is_default: boolean
is_active: boolean
created_at: string
updated_at: string
}
export interface CronJob {
id: number
agent_id: number
name: string
cron_expression: string
prompt: string
is_active: boolean
last_run_at?: string
next_run_at?: string
created_at: string
updated_at: string
}
export interface Channel {
id: number
type: string
name: string
config?: any
is_active: boolean
created_at: string
updated_at: string
}
+25
View File
@@ -0,0 +1,25 @@
<template>
<div class="agents">
<el-card>
<template #header>
<div class="card-header">
<span>Agent 管理</span>
<el-button type="primary" size="small">创建 Agent</el-button>
</div>
</template>
<el-empty description="Agent 管理功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
+24
View File
@@ -0,0 +1,24 @@
<template>
<div class="chat">
<el-card>
<template #header>
<div class="card-header">
<span>对话</span>
</div>
</template>
<el-empty description="对话功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div class="home">
<el-row :gutter="20">
<el-col :span="8">
<el-card>
<el-statistic title="工作空间" :value="stats.workspaces">
<template #suffix></template>
</el-statistic>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<el-statistic title="Agent" :value="stats.agents">
<template #suffix></template>
</el-statistic>
</el-card>
</el-col>
<el-col :span="8">
<el-card>
<el-statistic title="对话" :value="stats.conversations">
<template #suffix></template>
</el-statistic>
</el-card>
</el-col>
</el-row>
<el-card class="welcome-card" style="margin-top: 20px">
<h2>欢迎使用 SentClaw</h2>
<p>SentClaw 是一个基于 AI 的智能助手系统支持多 Agent 编排工具系统记忆管理等功能</p>
<el-button type="primary" @click="$router.push('/chat')">
开始对话
</el-button>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const stats = reactive({
workspaces: 0,
agents: 0,
conversations: 0
})
</script>
<style lang="scss" scoped>
.home {
h2 {
margin: 0 0 15px 0;
color: #303133;
}
p {
margin: 0 0 20px 0;
color: #606266;
line-height: 1.6;
}
}
</style>
+136
View File
@@ -0,0 +1,136 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<div class="card-header">
<h2>SentClaw</h2>
<p>AI 智能助手</p>
</div>
</template>
<el-form ref="loginFormRef" :model="loginForm" :rules="rules" @submit.prevent="handleLogin">
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
size="large"
prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
size="large"
prefix-icon="Lock"
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
style="width: 100%"
>
登录
</el-button>
</el-form-item>
</el-form>
<div class="register-link">
还没有账号<router-link to="/register">注册</router-link>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref()
const loading = ref(false)
const loginForm = reactive({
username: '',
password: ''
})
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
loading.value = true
await userStore.login(loginForm.username, loginForm.password)
ElMessage.success('登录成功')
router.push('/')
} catch (error: any) {
if (error.errors) {
return
}
ElMessage.error(error.message || '登录失败')
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
width: 400px;
}
.card-header {
text-align: center;
h2 {
margin: 0 0 10px 0;
color: #303133;
}
p {
margin: 0;
color: #909399;
}
}
.register-link {
text-align: center;
margin-top: 15px;
color: #606266;
a {
color: #409eff;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
</style>
+174
View File
@@ -0,0 +1,174 @@
<template>
<div class="register-container">
<el-card class="register-card">
<template #header>
<div class="card-header">
<h2>注册账号</h2>
<p>加入 SentClaw</p>
</div>
</template>
<el-form ref="registerFormRef" :model="registerForm" :rules="rules" @submit.prevent="handleRegister">
<el-form-item prop="username">
<el-input
v-model="registerForm.username"
placeholder="用户名"
size="large"
prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="email">
<el-input
v-model="registerForm.email"
placeholder="邮箱(可选)"
size="large"
prefix-icon="Message"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="registerForm.password"
type="password"
placeholder="密码"
size="large"
prefix-icon="Lock"
/>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input
v-model="registerForm.confirmPassword"
type="password"
placeholder="确认密码"
size="large"
prefix-icon="Lock"
@keyup.enter="handleRegister"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
@click="handleRegister"
style="width: 100%"
>
注册
</el-button>
</el-form-item>
</el-form>
<div class="login-link">
已有账号<router-link to="/login">登录</router-link>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
const registerFormRef = ref()
const loading = ref(false)
const registerForm = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
if (value !== registerForm.password) {
callback(new Error('两次输入的密码不一致'))
} else {
callback()
}
}
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
confirmPassword: [
{ required: true, message: '请确认密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}
const handleRegister = async () => {
if (!registerFormRef.value) return
try {
await registerFormRef.value.validate()
loading.value = true
await userStore.register(
registerForm.username,
registerForm.password,
registerForm.email || undefined
)
ElMessage.success('注册成功')
router.push('/')
} catch (error: any) {
if (error.errors) {
return
}
ElMessage.error(error.message || '注册失败')
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.register-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.register-card {
width: 400px;
}
.card-header {
text-align: center;
h2 {
margin: 0 0 10px 0;
color: #303133;
}
p {
margin: 0;
color: #909399;
}
}
.login-link {
text-align: center;
margin-top: 15px;
color: #606266;
a {
color: #409eff;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
</style>
+17
View File
@@ -0,0 +1,17 @@
<template>
<div class="settings">
<el-card>
<template #header>
<span>设置</span>
</template>
<el-empty description="设置功能开发中..." />
</el-card>
</div>
</template>
<script setup lang="ts">
</script>
<style lang="scss" scoped>
</style>
+13
View File
@@ -0,0 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.ts"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}
}
+33
View File
@@ -0,0 +1,33 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: {
'element-plus': ['element-plus'],
'vue-vendor': ['vue', 'vue-router', 'pinia']
}
}
}
}
})