重新提交项目内容
This commit is contained in:
parent
cd4e1d7e54
commit
eb77fb229d
|
|
@ -1,8 +0,0 @@
|
|||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# 开发环境标识(Vite 内置变量,无需修改)
|
||||
NODE_ENV = development
|
||||
|
||||
# 开发环境接口基础地址(对应你配置的 192.168.10.10:8085/api)
|
||||
VITE_API_BASE_URL = http://192.168.10.10:8085/api
|
||||
|
||||
# 请求超时时间(毫秒)
|
||||
VITE_REQUEST_TIMEOUT = 10000
|
||||
|
||||
# 可选:开发环境其他配置(如是否开启调试)
|
||||
VITE_DEBUG = true
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# 生产环境标识(Vite 内置变量,无需修改)
|
||||
NODE_ENV = production
|
||||
|
||||
# 生产环境接口基础地址
|
||||
VITE_API_BASE_URL = https://api.yourdomain.com
|
||||
|
||||
# 请求超时时间(毫秒)
|
||||
VITE_REQUEST_TIMEOUT = 10000
|
||||
|
||||
# 可选:生产环境其他配置(如关闭调试)
|
||||
VITE_DEBUG = false
|
||||
|
|
@ -1 +0,0 @@
|
|||
* text=auto eol=lf
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["eslint", "typescript", "unicorn", "oxc", "vue"],
|
||||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
48
README.md
48
README.md
|
|
@ -1,48 +0,0 @@
|
|||
# admin-system
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
pnpm lint
|
||||
```
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||
import skipFormatting from 'eslint-config-prettier/flat'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{vue,ts,mts,tsx}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
...pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
|
||||
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
|
||||
|
||||
skipFormatting,
|
||||
)
|
||||
13
index.html
13
index.html
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin System</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
55
package.json
55
package.json
|
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
"name": "admin-system",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "run-s lint:*",
|
||||
"lint:oxlint": "oxlint . --fix",
|
||||
"lint:eslint": "eslint . --fix --cache",
|
||||
"format": "prettier --write --experimental-cli src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^6.8.0",
|
||||
"@casl/vue": "^2.2.6",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.13.2",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.29",
|
||||
"vue-router": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^24.10.13",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@vue/tsconfig": "^0.8.1",
|
||||
"eslint": "^10.0.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-oxlint": "~1.50.0",
|
||||
"eslint-plugin-vue": "~10.8.0",
|
||||
"jiti": "^2.6.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.50.0",
|
||||
"prettier": "3.8.1",
|
||||
"sass": "^1.97.3",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-vue-devtools": "^8.0.6",
|
||||
"vue-tsc": "^3.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
4084
pnpm-lock.yaml
4084
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
12
src/App.vue
12
src/App.vue
|
|
@ -1,12 +0,0 @@
|
|||
<template>
|
||||
<div class="app-container">
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
183
src/api/index.ts
183
src/api/index.ts
|
|
@ -1,183 +0,0 @@
|
|||
import axios from 'axios'
|
||||
import type { AxiosResponse, AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||
import type { ApiResponse } from '@/api/types'
|
||||
import { ElLoading, ElNotification } from 'element-plus'
|
||||
import { getStorage, setStorage } from '@/utils/storage'
|
||||
|
||||
// 定义loading实例类型,用于控制loading的显示和关闭
|
||||
let loadingInstance: ReturnType<typeof ElLoading.service> | null = null
|
||||
// 记录当前请求数量,防止多个请求重复显示/关闭loading
|
||||
let requestCount = 0
|
||||
|
||||
// 显示loading的方法
|
||||
const showLoading = () => {
|
||||
if (requestCount === 0 && !loadingInstance) {
|
||||
loadingInstance = ElLoading.service({
|
||||
lock: true, // 锁定屏幕滚动
|
||||
fullscreen: true // 全屏显示
|
||||
})
|
||||
}
|
||||
requestCount++
|
||||
}
|
||||
|
||||
// 关闭loading的方法
|
||||
const hideLoading = () => {
|
||||
requestCount--
|
||||
if (requestCount <= 0) {
|
||||
if (loadingInstance) {
|
||||
loadingInstance.close()
|
||||
loadingInstance = null
|
||||
}
|
||||
requestCount = 0 // 重置计数,防止负数
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 环境自动识别 + 配置读取(核心修改:适配Vite环境变量)
|
||||
const env = import.meta.env
|
||||
const baseURL = env.VITE_API_BASE_URL || ''
|
||||
const timeout = Number(env.VITE_REQUEST_TIMEOUT) || 10000
|
||||
|
||||
// 2. 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL,
|
||||
timeout,
|
||||
headers: { 'Content-Type': 'application/json;charset=utf-8' },
|
||||
})
|
||||
|
||||
// 3. 请求拦截器:直接使用 axios 内置类型
|
||||
const requestInterceptor = (config: InternalAxiosRequestConfig) => {
|
||||
console.log('请求配置', config);
|
||||
|
||||
// 显示loading(可通过config配置项控制是否显示,比如某些请求不需要loading)
|
||||
if (config.headers?.showLoading !== false) {
|
||||
showLoading()
|
||||
}
|
||||
// 添加token到请求头
|
||||
const token = getStorage('token')
|
||||
if (token) {
|
||||
config.headers.token = token
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
const requestErrorInterceptor = (error: AxiosError) => {
|
||||
// 请求拦截器出错时也要关闭loading
|
||||
ElNotification({
|
||||
message: error.message || '未知的请求错误!',
|
||||
type: 'error',
|
||||
duration: 2000,
|
||||
})
|
||||
hideLoading()
|
||||
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
service.interceptors.request.use(requestInterceptor, requestErrorInterceptor)
|
||||
|
||||
// 4. 响应拦截器
|
||||
const responseInterceptor = <T = unknown>(response: AxiosResponse<ApiResponse<T>>) => {
|
||||
//关闭loading(无论成功失败都要关)
|
||||
hideLoading()
|
||||
if (response.headers['new_token'] !== undefined) {
|
||||
setStorage('token', response.headers['new_token'] as string)
|
||||
}
|
||||
console.log('响应response', response);
|
||||
const { data } = response
|
||||
console.log('响应数据data', data);
|
||||
// 判断业务状态码
|
||||
if (data.code == 200 || data.code == 0) {
|
||||
// 仅当业务成功时,才弹成功提示
|
||||
ElNotification({
|
||||
message: data.msg || '操作成功!',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
customClass: 'global-notification'
|
||||
})
|
||||
return data.data
|
||||
} else {
|
||||
if (data.code === 1 && data.msg === '用户不存在') {
|
||||
// 不弹错误提示,返回带特殊标识的结果
|
||||
// 其他业务失败:弹错误提示并拒绝Promise
|
||||
ElNotification({
|
||||
message: data.msg || '未知的错误!',
|
||||
type: 'error',
|
||||
duration: 2000,
|
||||
customClass: 'global-notification'
|
||||
})
|
||||
const result = {
|
||||
isUserNotExist: true, // 自定义标识,标记“用户不存在”
|
||||
} as T
|
||||
return result
|
||||
}
|
||||
|
||||
// 其他业务失败:弹错误提示并拒绝Promise
|
||||
ElNotification({
|
||||
message: data.msg || '未知的错误!',
|
||||
type: 'error',
|
||||
duration: 2000,
|
||||
customClass: 'global-notification'
|
||||
})
|
||||
return Promise.reject(data)
|
||||
}
|
||||
}
|
||||
|
||||
const responseErrorInterceptor = (error: AxiosError) => {
|
||||
// 响应失败,关闭loading
|
||||
hideLoading()
|
||||
|
||||
// 分类处理不同类型的网络错误
|
||||
let errMsg = ''
|
||||
if (error.message === 'Network Error') {
|
||||
errMsg = '网络异常,请检查您的网络连接!'
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
errMsg = `请求超时!请求耗时超过 ${timeout / 1000} 秒`
|
||||
} else if (error.response) {
|
||||
// 有响应状态码的错误
|
||||
const status = error.response.status
|
||||
switch (status) {
|
||||
case 401:
|
||||
errMsg = '登录状态过期,请重新登录!'
|
||||
// 可在此处添加跳转到登录页的逻辑
|
||||
break
|
||||
case 403:
|
||||
errMsg = '您没有权限访问该资源!'
|
||||
break
|
||||
case 404:
|
||||
errMsg = '请求的资源不存在!'
|
||||
break
|
||||
case 500:
|
||||
errMsg = '服务器内部错误,请稍后重试!'
|
||||
break
|
||||
default:
|
||||
errMsg = (error.response?.data as ApiResponse)?.msg || `请求失败:${status}`
|
||||
}
|
||||
} else {
|
||||
errMsg = error.message || '服务器错误'
|
||||
}
|
||||
|
||||
ElNotification({
|
||||
message: errMsg,
|
||||
type: 'error',
|
||||
duration: 2000,
|
||||
customClass: 'global-notification'
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
service.interceptors.response.use(responseInterceptor, responseErrorInterceptor)
|
||||
|
||||
// 5. 封装通用请求方法
|
||||
export const request = {
|
||||
get<T = unknown>(url: string, params?: unknown, config?: InternalAxiosRequestConfig): Promise<T> {
|
||||
return service.get<ApiResponse<T>>(url, { params, ...config }).then((res) => {
|
||||
return res as T;
|
||||
});
|
||||
},
|
||||
post<T = unknown>(url: string, data?: unknown, config?: InternalAxiosRequestConfig): Promise<T> {
|
||||
return service.post<ApiResponse<T>>(url, data, config).then((res) => {
|
||||
return res as T;
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
export default service
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { request } from '@/api';
|
||||
import type {
|
||||
MenuFormParams,
|
||||
DeleteMenuParams,
|
||||
MenuListResponse
|
||||
} from '@/api/types/menu';
|
||||
|
||||
/**
|
||||
* 获取菜单列表
|
||||
* @returns Promise<MenuListResponse> 菜单列表数据
|
||||
*/
|
||||
export async function getMenuApi(): Promise<MenuListResponse> {
|
||||
return request.get<MenuListResponse>('/menus/list');
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加菜单
|
||||
* @param params 菜单表单参数(form-data格式)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function addMenuApi(params: MenuFormParams): Promise<void> {
|
||||
return request.post<void>('/menus/create', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑菜单
|
||||
* @param params 菜单表单参数(含id)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function editMenuApi(params: MenuFormParams): Promise<void> {
|
||||
return request.post<void>('/menus/edit', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
* @param params 仅需菜单id
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function deleteMenuApi(params: DeleteMenuParams): Promise<void> {
|
||||
return request.post<void>('/menus/del', params);
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { request } from '@/api';
|
||||
import type {
|
||||
RoleFormParams,
|
||||
DeleteRoleParams,
|
||||
SetRolePermissionParams,
|
||||
GetRolePermissionParams,
|
||||
RoleListResponse,
|
||||
PermissionIdsResponse
|
||||
} from '@/api/types/role';
|
||||
|
||||
/**
|
||||
* 获取角色列表
|
||||
* @returns Promise<RoleListResponse> 角色列表数据
|
||||
*/
|
||||
export async function getRoleList(): Promise<RoleListResponse> {
|
||||
return request.get<RoleListResponse>('/roles/list');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
* @param params 角色名称、标题
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function createRole(params: RoleFormParams): Promise<void> {
|
||||
return request.post<void>('/roles/create', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑角色
|
||||
* @param params 角色ID、名称、标题
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function editRole(params: RoleFormParams): Promise<void> {
|
||||
return request.post<void>('/roles/edit', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* @param params 角色ID
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function deleteRole(params: DeleteRoleParams): Promise<void> {
|
||||
return request.post<void>('/roles/del', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置角色权限
|
||||
* @param params 角色ID + 权限ID列表(逗号分隔字符串)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function setRolePermission(params: SetRolePermissionParams): Promise<void> {
|
||||
return request.post<void>('/roles/setPermission', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色权限
|
||||
* @param params 角色ID
|
||||
* @returns Promise<PermissionIdsResponse> 权限ID数组
|
||||
*/
|
||||
export async function getRolePermission(params: GetRolePermissionParams): Promise<PermissionIdsResponse> {
|
||||
return request.get<PermissionIdsResponse>('/roles/getPermission', { id: params.id });
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { request } from '@/api';
|
||||
import type {
|
||||
LoginParams,
|
||||
LoginResponse,
|
||||
RegisterParams,
|
||||
ForgotPasswordParams,
|
||||
ChangePasswordParams,
|
||||
} from '@/api/types/user';
|
||||
|
||||
/**
|
||||
* 登录接口
|
||||
* @param params 登录参数(邮箱/密码/登录类型等)
|
||||
* @returns Promise<LoginResponse> 登录成功返回token+用户信息
|
||||
*/
|
||||
export async function loginApi(params: LoginParams): Promise<LoginResponse> {
|
||||
return request.post<LoginResponse>('/login', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册接口
|
||||
* @param params 注册参数(邮箱/密码/验证码等)
|
||||
* @returns Promise<boolean> 注册成功返回true,失败抛错
|
||||
*/
|
||||
export async function registerApi(params: RegisterParams) {
|
||||
// 泛型指定为 boolean,匹配后端返回的注册成功标识
|
||||
return request.post<boolean>('/reg', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码
|
||||
* @param params 忘记密码参数(邮箱/手机号等)
|
||||
* @returns Promise<string> 返回验证码ID(用于验证)
|
||||
*/
|
||||
export async function forgotPasswordApi(params: ForgotPasswordParams) {
|
||||
return request.post<string>('/resetPassword', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param params 改密参数(验证码/新密码/邮箱等)
|
||||
* @returns Promise<boolean> 改密成功返回true
|
||||
*/
|
||||
export async function changePasswordApi(params: ChangePasswordParams) {
|
||||
return request.post<boolean>('/user/editLoginPass', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @returns Promise<void> 无返回值
|
||||
*/
|
||||
export async function logoutApi() {
|
||||
return request.post<void>('/logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码
|
||||
* @param params 发送验证码参数(邮箱/手机号等)
|
||||
* @returns Promise<string> 返回验证码ID(用于验证)
|
||||
*/
|
||||
export async function sendCodeApi(params: SendCodeParams) {
|
||||
return request.post<string>('/sendCode', params);
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// src/utils/request.types.ts
|
||||
import type {
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
AxiosError,
|
||||
InternalAxiosRequestConfig // 导入内部类型
|
||||
} from 'axios'
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
isUserNotExist?: boolean
|
||||
}
|
||||
|
||||
// 修正拦截器类型,使用 InternalAxiosRequestConfig
|
||||
export type RequestInterceptor = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>
|
||||
export type RequestErrorInterceptor = (error: AxiosError) => Promise<AxiosError>
|
||||
export type ResponseInterceptor = <T = unknown>(response: AxiosResponse<ApiResponse<T>>) => T | Promise<T>
|
||||
export type ResponseErrorInterceptor = (error: AxiosError) => Promise<AxiosError>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import type { ApiResponse } from './index';
|
||||
|
||||
/** 菜单基础参数类型(新增/编辑通用) */
|
||||
export interface MenuFormParams {
|
||||
name: string; // 菜单名称
|
||||
path: string; // 前端路由
|
||||
api: string; // 后端接口路由
|
||||
icon: string; // 菜单图标
|
||||
parent_id: string; // 上级菜单ID(string类型,匹配后端)
|
||||
sort: string; // 排序(string类型)
|
||||
type: string; // 类型:1菜单2按钮(string类型)
|
||||
status?: string; // 状态:1启用2禁用(可选,编辑时用)
|
||||
id?: string; // 菜单ID(仅编辑/删除时传)
|
||||
}
|
||||
|
||||
/** 删除菜单参数类型 */
|
||||
export interface DeleteMenuParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** 菜单列表响应类型(单条菜单数据) */
|
||||
export interface MenuItem {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
api: string;
|
||||
icon: string;
|
||||
parent_id: string;
|
||||
sort: string;
|
||||
type: string;
|
||||
status: string;
|
||||
create_time?: string;
|
||||
update_time?: string;
|
||||
}
|
||||
|
||||
/** 菜单列表响应类型(泛型约束) */
|
||||
export type MenuListResponse = MenuItem[];
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import type { ApiResponse } from './index';
|
||||
|
||||
// 角色基础参数(创建/编辑通用)
|
||||
export interface RoleFormParams {
|
||||
name: string; // 角色名称(唯一)
|
||||
title: string; // 角色标题
|
||||
id?: string; // 仅编辑/删除/权限操作时需要
|
||||
}
|
||||
|
||||
// 删除角色参数
|
||||
export interface DeleteRoleParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// 设置角色权限参数
|
||||
export interface SetRolePermissionParams {
|
||||
id: string;
|
||||
permission_ids: string; // 权限ID,逗号分隔的字符串,如 "1,2,3,4"
|
||||
}
|
||||
|
||||
// 获取角色权限参数
|
||||
export interface GetRolePermissionParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// 角色列表单条数据类型
|
||||
export interface RoleItem {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
create_time?: string;
|
||||
update_time?: string;
|
||||
}
|
||||
|
||||
// 角色列表响应类型
|
||||
export type RoleListResponse = RoleItem[];
|
||||
|
||||
// 权限ID列表响应类型
|
||||
export type PermissionIdsResponse = string[];
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
// 登录参数类型
|
||||
export interface LoginParams {
|
||||
email?: string
|
||||
mobile?: string // 手机号(可选)
|
||||
password: string
|
||||
login_type: number // 1-邮箱登录 2-手机登录
|
||||
code?: string // 验证码(可选)
|
||||
}
|
||||
// 登录响应类型
|
||||
export interface LoginResponse {
|
||||
user: UserInfo
|
||||
menus_info: MenuItem[]
|
||||
permissions_info: PermissionItem[]
|
||||
isUserNotExist?: boolean // 自定义标识,标记“用户不存在”
|
||||
}
|
||||
// 注册参数类型
|
||||
export interface RegisterParams {
|
||||
email?: string // 邮箱(可选)
|
||||
mobile?: string // 手机号(可选)
|
||||
password: string
|
||||
reg_type?: number // 1-邮箱注册 2-手机注册
|
||||
invite_code?: string // 邀请码(可选)
|
||||
code: string
|
||||
|
||||
}
|
||||
|
||||
//忘记密码参数类型
|
||||
export interface ForgotPasswordParams {
|
||||
mobile?: string // 手机号(可选)
|
||||
email?: string // 邮箱(可选)
|
||||
reg_type: number // 1-邮箱登录 2-手机登录
|
||||
code: string
|
||||
login_password1: string
|
||||
login_password2: string
|
||||
}
|
||||
// 修改密码参数类型
|
||||
export interface ChangePasswordParams {
|
||||
mobile?: string // 手机号(可选)
|
||||
email?: string // 邮箱(可选)
|
||||
reg_type: number // 1-邮箱登录 2-手机登录
|
||||
code: string
|
||||
login_password1: string
|
||||
login_password2: string
|
||||
}
|
||||
// 发送验证码参数类型
|
||||
export interface SendCodeParams {
|
||||
mobile?: string // 手机号(可选)
|
||||
email?: string // 邮箱(可选)
|
||||
reg_type: number // 1-邮箱登录 2-手机登录
|
||||
type: number // 验证码类型
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.7 KiB |
|
|
@ -1,104 +0,0 @@
|
|||
//user styles
|
||||
.user-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
|
||||
.user-card {
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.logo {
|
||||
width: 115px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-form {
|
||||
// .el-form-item {
|
||||
// margin-bottom: 24px;
|
||||
// }
|
||||
|
||||
.phone-item {
|
||||
|
||||
.el-select {
|
||||
width: 120px;
|
||||
margin-right: 10px;
|
||||
|
||||
.country-option {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-submit {
|
||||
margin-bottom: 0;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.captcha-item {
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// tab切换表单动画
|
||||
.form-switch-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(10px) scale(0.98);
|
||||
}
|
||||
|
||||
.form-switch-enter-active {
|
||||
transition: all 0.3s ease-out;
|
||||
}
|
||||
|
||||
.form-switch-enter-to {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
|
||||
.form-switch-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
}
|
||||
|
||||
.form-switch-leave-active {
|
||||
transition: all 0.2s ease-in;
|
||||
}
|
||||
|
||||
.form-switch-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px) scale(0.98);
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<!-- src/components/LanguageSelect.vue -->
|
||||
<template>
|
||||
<!-- 独立的语言选择器 -->
|
||||
<el-select v-model="innerValue" placeholder="Select" style="width: 120px" @change="handleChange">
|
||||
<el-option v-for="item in languageOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 1. 定义语言类型(TS约束,和原有逻辑一致)
|
||||
type LanguageType = 'zh-CN' | 'zh-HK' | 'en-US'
|
||||
|
||||
// 2. 定义Props:接收父组件传入的选中值(支持v-model)
|
||||
const props = defineProps<{
|
||||
modelValue: LanguageType // v-model 绑定的核心prop
|
||||
}>()
|
||||
|
||||
// 3. 定义Emits:触发父组件更新值(实现v-model双向绑定)
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: LanguageType): void // v-model 必备emit
|
||||
(e: 'change', value: LanguageType): void // 可选:暴露change事件给父组件
|
||||
}>()
|
||||
|
||||
// 4. 内部响应式值(关联父组件的modelValue)
|
||||
const innerValue = ref<LanguageType>(props.modelValue)
|
||||
|
||||
// 5. 监听父组件值变化,同步到内部(避免单向数据流问题)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
innerValue.value = newVal
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 6. 语言选项(组件内封装,也可通过props传入增强灵活性)
|
||||
const languageOptions = [
|
||||
{ value: 'zh-CN' as LanguageType, label: '简体中文' },
|
||||
{ value: 'zh-HK' as LanguageType, label: '繁体中文' },
|
||||
{ value: 'en-US' as LanguageType, label: 'English' }
|
||||
]
|
||||
|
||||
// 7. 选择变化时触发emit,同步给父组件
|
||||
const handleChange = (value: LanguageType) => {
|
||||
emit('update:modelValue', value) // 实现v-model双向绑定
|
||||
emit('change', value) // 可选:额外暴露change事件
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type LanguageType = 'zh-CN' | 'zh-HK' | 'en-US'
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<!-- src/components/layout/Layout.vue -->
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<!-- 左侧菜单 -->
|
||||
<Sidebar />
|
||||
<!-- 右侧主内容区 -->
|
||||
<div class="main-content">
|
||||
<Header />
|
||||
<!-- 路由视图:渲染当前路由对应的页面(如dashboard/index.vue) -->
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
16
src/main.ts
16
src/main.ts
|
|
@ -1,16 +0,0 @@
|
|||
// css和scss
|
||||
import './assets/styles/global.scss'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { getStorage, setStorage } from '@/utils/storage'
|
||||
|
||||
export type LanguageType = 'zh-CN' | 'zh-HK' | 'en-US'
|
||||
const langMap: Record<LanguageType, string> = {
|
||||
'zh-CN': 'zh-CN', // 简体中文(中国大陆)
|
||||
'zh-HK': 'zh-HK', // 繁体中文(中国香港)
|
||||
'en-US': 'en-US' // 英语(美国)
|
||||
}
|
||||
|
||||
export const useLanguageStore = defineStore('language', () => {
|
||||
const initLang = getStorage('lang') as LanguageType || 'zh-CN'
|
||||
const lang = ref<LanguageType>(initLang)
|
||||
|
||||
if (!getStorage('lang')) {
|
||||
setStorage('lang', initLang)
|
||||
}
|
||||
// const setHtmlLang = (currentLang: LanguageType) => {
|
||||
// document.documentElement.lang = langMap[currentLang]
|
||||
// }
|
||||
|
||||
// setHtmlLang(lang.value)
|
||||
|
||||
const changeLang = (newLang: LanguageType) => {
|
||||
lang.value = newLang
|
||||
setStorage('lang', newLang)
|
||||
// setHtmlLang(newLang)
|
||||
}
|
||||
|
||||
return { lang, changeLang }
|
||||
})
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
import { getStorage, setStorage, removeStorage } from '@/utils/storage';
|
||||
import { loginApi, registerApi, logoutApi, forgotPasswordApi, changePasswordApi, sendCodeApi } from '@/api/modules/user';
|
||||
import type { LoginParams, RegisterParams, ForgotPasswordParams, ChangePasswordParams, SendCodeParams } from '@/api/types/user';
|
||||
import type { UserInfo, MenuItem, PermissionItem } from '@/stores/types/user';
|
||||
|
||||
// 定义用户 Store
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
const router = useRouter();
|
||||
|
||||
// ========== 状态定义(与后端返回数据对应) ==========
|
||||
const userInfo = ref<UserInfo | null>(null);
|
||||
const menus = ref<MenuItem[]>([]);
|
||||
const permissions = ref<PermissionItem[]>([]);
|
||||
const token = ref<string>(getStorage('token') || '');
|
||||
|
||||
// ========== 计算属性 ==========
|
||||
const isLoggedIn = computed(() => !!token.value);
|
||||
const permissionKeys = computed(() => permissions.value.map(item => item.name));
|
||||
|
||||
// ========== 核心方法(全部改为箭头函数) ==========
|
||||
/**
|
||||
* 登录方法
|
||||
* @param params 登录参数
|
||||
*/
|
||||
const login = async (params: LoginParams) => {
|
||||
try {
|
||||
const res = await loginApi(params);
|
||||
if(res.isUserNotExist) {
|
||||
// 用户不存在,跳转到注册页面
|
||||
router.push('/user/register');
|
||||
return;
|
||||
}
|
||||
token.value = res.user.token;
|
||||
userInfo.value = res.user;
|
||||
menus.value = res.menus_info;
|
||||
permissions.value = res.permissions_info;
|
||||
|
||||
setStorage('token', res.user.token);
|
||||
setStorage('userInfo', res.user);
|
||||
// 跳转到首页
|
||||
router.push('/');
|
||||
} catch (error) {
|
||||
console.error('登录失败', error);
|
||||
|
||||
throw error;
|
||||
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 注册方法
|
||||
* @param params 登录参数
|
||||
*/
|
||||
|
||||
const register = async (params: RegisterParams) => {
|
||||
try {
|
||||
const res = await registerApi(params);
|
||||
//跳转登录页面
|
||||
router.push('/user/login');
|
||||
} catch (error) {
|
||||
console.error('注册失败', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
await logoutApi();
|
||||
} catch (error) {
|
||||
console.error('退出登录接口调用失败', error);
|
||||
} finally {
|
||||
token.value = '';
|
||||
userInfo.value = null;
|
||||
menus.value = [];
|
||||
permissions.value = [];
|
||||
|
||||
removeStorage('token');
|
||||
removeStorage('userInfo');
|
||||
|
||||
// 跳转到登录页面
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 忘记密码
|
||||
*/
|
||||
const forgotPassword = async (params: ForgotPasswordParams) => {
|
||||
try {
|
||||
const res = await forgotPasswordApi(params);
|
||||
// 跳转登录页面
|
||||
router.push('/user/login');
|
||||
} catch (error) {
|
||||
console.error('忘记密码失败', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
const changePassword = async (params: ChangePasswordParams) => {
|
||||
try {
|
||||
const res = await changePasswordApi(params);
|
||||
// 跳转登录页面
|
||||
removeStorage('token');
|
||||
removeStorage('userInfo');
|
||||
router.push('/user/login');
|
||||
} catch (error) {
|
||||
console.error('修改密码失败', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 发送验证码
|
||||
*/
|
||||
const sendCode = async (params: SendCodeParams) => {
|
||||
try {
|
||||
const res = await sendCodeApi(params);
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
return {
|
||||
// 状态
|
||||
token,
|
||||
userInfo,
|
||||
menus,
|
||||
permissions,
|
||||
// 计算属性
|
||||
isLoggedIn,
|
||||
permissionKeys,
|
||||
// 方法(箭头函数)
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
forgotPassword,
|
||||
changePassword,
|
||||
sendCode,
|
||||
};
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import type { ApiResponse } from './index';
|
||||
|
||||
// 用户信息子类型
|
||||
export interface UserInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
mobile: string;
|
||||
role_id: number;
|
||||
pass_admin: string;
|
||||
salt_code_admin: string;
|
||||
real_check: number;
|
||||
par_uid: number;
|
||||
path: string;
|
||||
lang_type: number;
|
||||
reg_type: number;
|
||||
status: number;
|
||||
invite_code: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
deleted_at: string | null;
|
||||
token: string;
|
||||
}
|
||||
|
||||
// 菜单子项类型
|
||||
export interface MenuChildItem {
|
||||
id: number;
|
||||
name: string;
|
||||
api: string;
|
||||
path: string | null;
|
||||
icon: string | null;
|
||||
parent_id: number;
|
||||
sort: number;
|
||||
type: number;
|
||||
status: number;
|
||||
}
|
||||
|
||||
// 菜单主项类型
|
||||
export interface MenuItem {
|
||||
id: number;
|
||||
name: string;
|
||||
api: string;
|
||||
path: string | null;
|
||||
icon: string | null;
|
||||
parent_id: number;
|
||||
sort: number;
|
||||
type: number;
|
||||
status: number;
|
||||
son: MenuChildItem[];
|
||||
}
|
||||
|
||||
// 权限信息类型
|
||||
export interface PermissionItem {
|
||||
id: number;
|
||||
name: string;
|
||||
title: string;
|
||||
module: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
// 登录响应数据类型
|
||||
export interface LoginResponseData {
|
||||
user: UserInfo;
|
||||
menus_info: MenuItem[];
|
||||
permissions_info: PermissionItem[];
|
||||
}
|
||||
|
||||
// 登录接口响应类型(适配 ApiResponse 泛型)
|
||||
export type LoginResponse = LoginResponseData;
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import CryptoJS from 'crypto-js'
|
||||
|
||||
/**
|
||||
* MD5 加密函数
|
||||
* @param str 要加密的字符串
|
||||
* @returns 加密后的 32 位小写字符串
|
||||
*/
|
||||
export const md5Encrypt = (str: string): string => {
|
||||
if (!str) return ''
|
||||
// 核心:调用 crypto-js 的 MD5 方法,转换为小写字符串
|
||||
return CryptoJS.MD5(str).toString(CryptoJS.enc.Hex).toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 加密(大写)
|
||||
* @param str 要加密的字符串
|
||||
* @returns 加密后的 32 位大写字符串
|
||||
*/
|
||||
// export const md5EncryptUpper = (str: string): string => {
|
||||
// return md5Encrypt(str).toUpperCase()
|
||||
// }
|
||||
|
||||
/**
|
||||
* MD5 加盐加密(提升安全性)
|
||||
* @param str 要加密的字符串
|
||||
* @param salt 盐值(自定义,如项目唯一标识)
|
||||
* @returns 加盐后的 MD5 字符串
|
||||
*/
|
||||
// export const md5EncryptWithSalt = (str: string, salt: string = 'your-custom-salt'): string => {
|
||||
// // 盐值拼接方式可自定义,如 str + salt 或 salt + str
|
||||
// return md5Encrypt(`${str}${salt}`)
|
||||
// }
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
// src/utils/storage.ts
|
||||
import type { Ref } from 'vue'
|
||||
import { useStorage as useVueUseStorage } from '@vueuse/core'
|
||||
|
||||
// 项目前缀(避免存储键冲突)
|
||||
const PROJECT_PREFIX = 'foreign_'
|
||||
type StorageType = 'local' | 'session'
|
||||
|
||||
/**
|
||||
* 给key添加项目前缀
|
||||
*/
|
||||
const addPrefix = (key: string): string => `${PROJECT_PREFIX}${key}`
|
||||
|
||||
/**
|
||||
* 获取对应的 Storage 实例
|
||||
*/
|
||||
const getStorageInstance = (type: StorageType = 'local') => {
|
||||
return type === 'local' ? localStorage : sessionStorage
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用方法:自动反序列化存储值
|
||||
*/
|
||||
const autoParse = (value: string | null) => {
|
||||
if (value === null) return undefined
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增/改:设置存储值(默认localStorage,自动加前缀)
|
||||
*/
|
||||
export const setStorage = <T = unknown>(
|
||||
key: string,
|
||||
value: T,
|
||||
type: StorageType = 'local'
|
||||
): void => {
|
||||
const prefixedKey = addPrefix(key)
|
||||
const storage = getStorageInstance(type)
|
||||
let storedValue: string
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
storedValue = JSON.stringify(value)
|
||||
} else {
|
||||
storedValue = String(value)
|
||||
}
|
||||
storage.setItem(prefixedKey, storedValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查:获取存储值(默认localStorage,自动加前缀,无需默认值)
|
||||
*/
|
||||
export const getStorage = <T = unknown>(
|
||||
key: string,
|
||||
type: StorageType = 'local',
|
||||
defaultValue?: T
|
||||
): T | undefined => {
|
||||
const prefixedKey = addPrefix(key)
|
||||
const storage = getStorageInstance(type)
|
||||
const rawValue = storage.getItem(prefixedKey)
|
||||
return rawValue === null ? defaultValue : (autoParse(rawValue) as T)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删:删除指定键
|
||||
*/
|
||||
export const removeStorage = (
|
||||
key: string,
|
||||
type: StorageType = 'local'
|
||||
): void => {
|
||||
const prefixedKey = addPrefix(key)
|
||||
getStorageInstance(type).removeItem(prefixedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空:只清空当前项目前缀的存储
|
||||
*/
|
||||
export const clearStorage = (type: StorageType = 'local'): void => {
|
||||
const storage = getStorageInstance(type)
|
||||
Object.keys(storage).forEach(key => {
|
||||
if (key.startsWith(PROJECT_PREFIX)) {
|
||||
storage.removeItem(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应式存储引用(兼容自动反序列化)
|
||||
*/
|
||||
export const useStorage = <T = unknown>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
type: StorageType = 'local'
|
||||
): Ref<T> => {
|
||||
const prefixedKey = addPrefix(key)
|
||||
return useVueUseStorage(
|
||||
prefixedKey,
|
||||
defaultValue,
|
||||
getStorageInstance(type),
|
||||
{
|
||||
serializer: {
|
||||
read: autoParse,
|
||||
write: (v: unknown) => {
|
||||
if (typeof v === 'object' && v !== null) {
|
||||
return JSON.stringify(v)
|
||||
}
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
) as Ref<T>
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
<template>
|
||||
<Layout>
|
||||
</Layout>
|
||||
</template>
|
||||
|
|
@ -1,356 +0,0 @@
|
|||
<template>
|
||||
<div class="user-card">
|
||||
<UserHeader />
|
||||
<UserText :user-text="userText" @back="handleBack" />
|
||||
<UserTabs v-model="activeTab" :hide-tab="hideTab" :tab-list="tabList" />
|
||||
|
||||
<el-form class="user-form" ref="forgotPasswordFormRef" :model="forgotPasswordForm" :rules="forgotPasswordRules">
|
||||
<Transition name="form-switch" mode="out-in">
|
||||
<el-form-item v-if="activeTab === 'email'" key="form-email-item" prop="email">
|
||||
<el-input v-model="forgotPasswordForm.email" placeholder="请输入邮箱" clearable size="large" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="phone-item" prop="phone" v-else-if="activeTab === 'phone'" key="form-phone-item">
|
||||
<el-select v-model="forgotPasswordForm.countryCode" :teleported="false" value-key="code" placeholder="国家"
|
||||
size="large">
|
||||
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code">
|
||||
<div class="country-option">
|
||||
<span class="country-name">{{ item.name }}</span>
|
||||
<span class="country-code">{{ item.code }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input v-model="forgotPasswordForm.phone" placeholder="请输入手机号" clearable size="large" />
|
||||
</el-form-item>
|
||||
<UserCaptcha v-model="forgotPasswordForm.code" v-else-if="activeTab === 'code'" key="form-code-item"
|
||||
@sendCode="handleSendCode" :show-send-button="showSendButton" :send-state="sendState" />
|
||||
<div v-else-if="activeTab === 'password'" key="form-password-group">
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="forgotPasswordForm.password" placeholder="请输入新密码" type="password" show-password clearable
|
||||
size="large" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input v-model="forgotPasswordForm.confirmPassword" placeholder="请确认新密码" type="password" show-password
|
||||
clearable size="large" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="forgot-next">
|
||||
<div :class="{ active: activeTab === 'email' || activeTab === 'phone' }"></div>
|
||||
<div :class="{ active: activeTab === 'code' }"></div>
|
||||
<div :class="{ active: activeTab === 'password' }"></div>
|
||||
</div>
|
||||
<el-form-item class="form-submit">
|
||||
<el-button type="primary" @click="handleForgotPassword">{{ activeTab === 'password' ? '去登录' : '继续'
|
||||
}}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// type类型
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
// 组件
|
||||
import UserHeader from './components/Header.vue'
|
||||
import UserText from './components/Text.vue'
|
||||
import UserTabs from './components/Tabs.vue'
|
||||
import UserCaptcha from './components/Captcha.vue'
|
||||
|
||||
//公共方法/store/配置文件
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { setStorage, getStorage, removeStorage } from '@/utils/storage'
|
||||
import { md5Encrypt } from '@/utils/crypto'
|
||||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
|
||||
|
||||
//用户校验规则
|
||||
import { countryOptions } from '@/views/user/config/mock'
|
||||
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
|
||||
|
||||
//用户store和router实例
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
//userText 实例
|
||||
const userText = ref({
|
||||
backText: '返回',
|
||||
title: '恢复密码',
|
||||
text: '请输入您注册时使用的邮箱或手机号',
|
||||
account: '',
|
||||
|
||||
})
|
||||
|
||||
//tabs实例
|
||||
const tabList = ref([
|
||||
{ label: '邮箱', name: 'email' },
|
||||
{ label: '手机', name: 'phone' },
|
||||
])
|
||||
const activeTab = ref<string>('email')
|
||||
const hideTab = ref<boolean>(false)
|
||||
|
||||
// 表单实例
|
||||
const forgotPasswordFormRef = ref<FormInstance>()
|
||||
const forgotPasswordForm = ref({
|
||||
email: '',
|
||||
countryCode: '+86',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: ''
|
||||
})
|
||||
const forgotPasswordRules = ref<FormRules>({
|
||||
email: emailRules().email,
|
||||
phone: phoneRules(forgotPasswordForm.value).phone,
|
||||
password: passwordRules(forgotPasswordForm.value).password,
|
||||
confirmPassword: passwordRules(forgotPasswordForm.value).confirmPassword,
|
||||
code: codeRules().code
|
||||
})
|
||||
|
||||
// 验证码实例
|
||||
const showSendButton = ref<boolean>(true)
|
||||
const sendState = ref<object>({
|
||||
isCounting: false,
|
||||
countdown: 0,
|
||||
})
|
||||
//初始化验证码倒计时状态
|
||||
const emailCaptchaState = initCaptchaState('email'); // 邮箱专属
|
||||
const phoneCaptchaState = initCaptchaState('phone'); // 手机专属
|
||||
// 邮箱验证码状态
|
||||
const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState
|
||||
// 手机验证码状态
|
||||
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
|
||||
|
||||
//方法
|
||||
|
||||
|
||||
// 统一更新sendState的方法
|
||||
const updateSendState = (type: string) => {
|
||||
if (type === 'email') {
|
||||
sendState.value = {
|
||||
isCounting: emailIsCounting,
|
||||
countdown: emailCountdown
|
||||
}
|
||||
} else {
|
||||
sendState.value = {
|
||||
isCounting: phoneIsCounting,
|
||||
countdown: phoneCountdown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 监听activeTab变化,同步sendState
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'email' || newTab === 'phone') {
|
||||
// 切换回邮箱/手机页时,重置sendState
|
||||
sendState.value = { isCounting: false, countdown: 0 }
|
||||
} else if (newTab === 'code') {
|
||||
// 切换到验证码页时,根据当前存储的类型更新状态
|
||||
const currentType: string = getStorage('forgotType', 'session') || 'email'
|
||||
updateSendState(currentType)
|
||||
}
|
||||
})
|
||||
|
||||
// 定义发送验证码接口
|
||||
const sendCodeApi = async () => {
|
||||
// 发送验证码参数
|
||||
const sendParams = {
|
||||
email: forgotPasswordForm.value.email,
|
||||
mobile: forgotPasswordForm.value.phone,
|
||||
reg_type: getStorage('forgotType', 'session') === 'email' ? 1 : 2,
|
||||
type:'transfer_code'
|
||||
}
|
||||
return userStore.sendCode(sendParams)
|
||||
}
|
||||
|
||||
//发送验证码
|
||||
const handleSendCode = async () => {
|
||||
// 第二步:确定类型、接收方、对应的倒计时状态和发送函数
|
||||
const type: string = getStorage('forgotType', 'session') || 'email'
|
||||
const receiver = type === 'phone' ? forgotPasswordForm.value.phone : forgotPasswordForm.value.email;
|
||||
const currentCaptchaState = type === 'phone' ? phoneCaptchaState : emailCaptchaState;
|
||||
const sendFunc = type === 'phone' ? "sendPhoneCaptcha" : "sendEmailCaptcha";
|
||||
|
||||
// 第三步:调用封装的发送函数(传正确的函数,而非字符串)
|
||||
await sendCaptcha(
|
||||
type,
|
||||
receiver,
|
||||
currentCaptchaState, // 只更新当前类型的倒计时状态
|
||||
() => sendCodeApi() // 传入返回Promise的函数
|
||||
);
|
||||
|
||||
//把状态同步给子组件
|
||||
updateSendState(type)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const handleBack = () => {
|
||||
// 从密码页返回 → 验证码页
|
||||
if (activeTab.value === 'password') {
|
||||
activeTab.value = 'code'
|
||||
userText.value = {
|
||||
...userText.value,
|
||||
title: '双重验证',
|
||||
text: '请输入发送的验证码',
|
||||
account: getStorage('forgot', 'session') || ''
|
||||
}
|
||||
// 清空密码字段
|
||||
forgotPasswordForm.value.password = ''
|
||||
forgotPasswordForm.value.confirmPassword = ''
|
||||
forgotPasswordFormRef.value?.clearValidate(['password', 'confirmPassword'])
|
||||
}
|
||||
|
||||
// 从验证码页返回 → 邮箱/手机页
|
||||
else if (activeTab.value === 'code') {
|
||||
const getForgotType: string = getStorage('forgotType', 'session') || 'email'
|
||||
activeTab.value = getForgotType
|
||||
hideTab.value = false
|
||||
userText.value = {
|
||||
...userText.value,
|
||||
title: '恢复密码',
|
||||
text: '请输入您注册时使用的邮箱或手机号',
|
||||
account: ''
|
||||
}
|
||||
// 清空验证码字段
|
||||
forgotPasswordForm.value.code = ''
|
||||
forgotPasswordFormRef.value?.clearValidate(['code'])
|
||||
}
|
||||
|
||||
// 从邮箱/手机页返回 → 重置为初始状态
|
||||
else {
|
||||
activeTab.value = 'email'
|
||||
forgotPasswordForm.value.email = ''
|
||||
forgotPasswordForm.value.phone = ''
|
||||
forgotPasswordFormRef.value?.clearValidate(['email', 'phone'])
|
||||
router.push('/user/login')
|
||||
}
|
||||
}
|
||||
|
||||
//重置密码
|
||||
const handleForgotPassword = async () => {
|
||||
if (!forgotPasswordFormRef.value) return
|
||||
|
||||
try {
|
||||
const currentTab = activeTab.value;
|
||||
const getForgotType: string = getStorage('forgotType', 'session') || 'email'
|
||||
// 步骤1:校验邮箱/手机号 → 切换到验证码页
|
||||
if (currentTab === 'email' || currentTab === 'phone') {
|
||||
// 校验当前字段
|
||||
await forgotPasswordFormRef.value.validateField(currentTab)
|
||||
// 存储账号类型和值
|
||||
setStorage('forgotType', currentTab, 'session')
|
||||
setStorage('forgot', currentTab === 'email' ? forgotPasswordForm.value.email : forgotPasswordForm.value.phone, 'session')
|
||||
// 更新状态
|
||||
hideTab.value = true
|
||||
activeTab.value = 'code'
|
||||
userText.value = {
|
||||
...userText.value,
|
||||
title: '双重验证',
|
||||
text: '请输入发送的验证码',
|
||||
account: currentTab === 'email' ? forgotPasswordForm.value.email : forgotPasswordForm.value.phone
|
||||
}
|
||||
updateSendState(getForgotType)
|
||||
}
|
||||
|
||||
// 步骤2:校验验证码 → 切换到密码页
|
||||
else if (currentTab === 'code') {
|
||||
// 校验验证码
|
||||
await forgotPasswordFormRef.value.validateField(currentTab)
|
||||
// 更新状态
|
||||
activeTab.value = 'password'
|
||||
userText.value = {
|
||||
...userText.value,
|
||||
title: '设置新密码',
|
||||
text: '请设置您的新登录密码',
|
||||
account: getStorage('forgot', 'session') || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤3:校验密码 → 重置密码
|
||||
else if (currentTab === 'password') {
|
||||
// 校验密码+确认密码
|
||||
await forgotPasswordFormRef.value.validateField(['password', 'confirmPassword'])
|
||||
|
||||
// 重置密码参数
|
||||
const getForgotType = getStorage('forgotType', 'session') || 'email'
|
||||
const forgotParams = {
|
||||
mobile: forgotPasswordForm.value.phone,
|
||||
email: forgotPasswordForm.value.email,
|
||||
reg_type: getForgotType === 'email' ? 1 : 2,
|
||||
code: forgotPasswordForm.value.code,
|
||||
login_password1: md5Encrypt(forgotPasswordForm.value.password),
|
||||
login_password2: md5Encrypt(forgotPasswordForm.value.confirmPassword),
|
||||
}
|
||||
|
||||
// 调用重置密码接口
|
||||
await userStore.forgotPassword(forgotParams)
|
||||
|
||||
// 清空存储和表单
|
||||
removeStorage('forgotType', 'session')
|
||||
removeStorage('forgot', 'session')
|
||||
forgotPasswordForm.value = {
|
||||
email: '',
|
||||
countryCode: '+86',
|
||||
phone: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error)
|
||||
}
|
||||
}
|
||||
// 4. 组件卸载时清除定时器(关键:防止内存泄漏)
|
||||
onUnmounted(() => {
|
||||
clearCaptchaTimer(emailCaptchaState);
|
||||
clearCaptchaTimer(phoneCaptchaState);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-form {
|
||||
.phone-item {
|
||||
:deep(.el-form-item__content) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-next {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 60px;
|
||||
|
||||
div {
|
||||
position: relative;
|
||||
height: 4px;
|
||||
width: 40px;
|
||||
background: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
||||
border-radius: 999px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background-color: var(--el-color-primary);
|
||||
transition: all 0.3s ease-out;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
&::before {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,210 +0,0 @@
|
|||
<template>
|
||||
<div class="user-card">
|
||||
<UserHeader />
|
||||
<UserText :user-text="userText" @back="handleBack" />
|
||||
<UserTabs v-model="activeTab" :hide-tab="hideTab" :tab-list="tabList" />
|
||||
|
||||
<el-form class="user-form" ref="loginFormRef" :model="loginForm" :rules="loginRules">
|
||||
<Transition name="form-switch" mode="out-in">
|
||||
<el-form-item v-if="activeTab === 'email'" key="form-email-item" prop="email">
|
||||
<el-input v-model="loginForm.email" placeholder="请输入邮箱" clearable size="large" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="phone-item" prop="phone" v-else-if="activeTab === 'phone'" key="form-phone-item">
|
||||
<el-select v-model="loginForm.countryCode" :teleported="false" value-key="code" placeholder="国家" size="large">
|
||||
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code">
|
||||
<div class="country-option">
|
||||
<span class="country-name">{{ item.name }}</span>
|
||||
<span class="country-code">{{ item.code }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input v-model="loginForm.phone" placeholder="请输入手机号" clearable size="large" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-else-if="activeTab === 'password'" key="form-password-item" prop="password">
|
||||
<el-input v-model="loginForm.password" placeholder="请输入密码" type="password" show-password clearable
|
||||
size="large" />
|
||||
<router-link to="/user/forgot-password" class="forgot-password">忘记密码?</router-link>
|
||||
</el-form-item>
|
||||
</Transition>
|
||||
<el-form-item class="form-submit">
|
||||
<el-button type="primary" @click="handleLogin">{{ activeTab === 'password' ? '登录' : '注册/登录' }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// type类型
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
// 组件
|
||||
import UserHeader from './components/Header.vue'
|
||||
import UserText from './components/Text.vue'
|
||||
import UserTabs from './components/Tabs.vue'
|
||||
|
||||
//公共方法/store/配置文件
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { setStorage, getStorage } from '@/utils/storage'
|
||||
import { md5Encrypt } from '@/utils/crypto'
|
||||
|
||||
//用户校验规则
|
||||
import { countryOptions } from '@/views/user/config/mock'
|
||||
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
|
||||
|
||||
//userText 实例
|
||||
const userText = ref({
|
||||
backText: '',
|
||||
title: '',
|
||||
text: '初次登录时,您的电子邮箱或手机号码将被用于注册。',
|
||||
account: '',
|
||||
|
||||
})
|
||||
|
||||
//tabs 实例
|
||||
const tabList = ref([
|
||||
{ label: '邮箱', name: 'email' },
|
||||
{ label: '手机', name: 'phone' },
|
||||
])
|
||||
const activeTab = ref<string>('email')
|
||||
const hideTab = ref<boolean>(false)
|
||||
|
||||
// login form 实例
|
||||
const userStore = useUserStore();
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
const loginForm = ref({
|
||||
email: '',
|
||||
countryCode: '+86',
|
||||
phone: '',
|
||||
password: '',
|
||||
code: '123456',
|
||||
})
|
||||
const loginRules = ref<FormRules>({
|
||||
email: emailRules().email,
|
||||
phone: phoneRules(loginForm.value).phone,
|
||||
code: codeRules().code,
|
||||
password: passwordRules(loginForm.value).password,
|
||||
})
|
||||
|
||||
//方法
|
||||
|
||||
// 登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginFormRef.value) return;
|
||||
|
||||
try {
|
||||
const currentTab = activeTab.value;
|
||||
//验证邮箱/手机号 → 切换到密码页
|
||||
if (currentTab === 'email' || currentTab === 'phone') {
|
||||
//校验当前账号字段(email/phone)
|
||||
await loginFormRef.value.validateField(currentTab);
|
||||
//存储账号类型和账号值(方便后续登录取值)
|
||||
const loginValue = loginForm.value[currentTab];
|
||||
setStorage('loginType', currentTab, 'session'); // 存储账号类型(email/phone)
|
||||
setStorage('login', loginValue, 'session'); // 存储账号值
|
||||
//切换到密码页,更新提示
|
||||
hideTab.value = true;
|
||||
userText.value = {
|
||||
backText: '返回',
|
||||
title: '欢迎回来!',
|
||||
text: '请输入您的登录密码',
|
||||
account: loginValue
|
||||
};
|
||||
activeTab.value = 'password';
|
||||
}
|
||||
// 验证密码+验证码 → 执行登录
|
||||
else if (currentTab === 'password') {
|
||||
// 校验密码+验证码
|
||||
await loginFormRef.value.validateField(['password', 'code']);
|
||||
// 登录参数
|
||||
const loginParams = {
|
||||
email: loginForm.value.email,
|
||||
mobile: loginForm.value.phone,
|
||||
login_type: getStorage('loginType', 'session') === 'email' ? 1 : 2,
|
||||
code: loginForm.value.code,
|
||||
password: md5Encrypt(loginForm.value.password)
|
||||
};
|
||||
await userStore.login(loginParams);
|
||||
activeTab.value = 'email'; // 重置tab
|
||||
hideTab.value = false;
|
||||
loginForm.value = { // 清空表单
|
||||
email: '',
|
||||
countryCode: '+86',
|
||||
phone: '',
|
||||
password: '',
|
||||
code: ''
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('表单校验失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
//返回登录页
|
||||
const handleBack = () => {
|
||||
if (activeTab.value === 'password') {
|
||||
// 从sessionStorage取账号类型(而非用password作为key)
|
||||
const loginType = getStorage('loginType', 'session') || 'email';
|
||||
// 清空密码、验证码字段,避免残留
|
||||
loginForm.value.password = '';
|
||||
loginForm.value.code = '';
|
||||
// 清空表单校验提示
|
||||
loginFormRef.value?.clearValidate(['password', 'code']);
|
||||
// 重置文本和tab状态
|
||||
hideTab.value = false;
|
||||
userText.value = {
|
||||
backText: '',
|
||||
title: '',
|
||||
text: '初次登录时,您的电子邮箱或手机号码将被用于注册。',
|
||||
account: ''
|
||||
};
|
||||
activeTab.value = loginType as string;
|
||||
} else {
|
||||
// 非password页:仅重置为email(可选,也可保留当前tab)
|
||||
activeTab.value = 'email';
|
||||
// 清空当前账号字段和校验提示
|
||||
loginForm.value.email = '';
|
||||
loginForm.value.phone = '';
|
||||
loginFormRef.value?.clearValidate(['email', 'phone']);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-form {
|
||||
.phone-item {
|
||||
:deep(.el-form-item__content) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 120px;
|
||||
margin-right: 10px;
|
||||
|
||||
.country-option {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.el-select-dropdown__item) {
|
||||
padding: 0 16px 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
font-size: 14px;
|
||||
color: #409eff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.form-submit {
|
||||
margin-top: 18px;
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
<template>
|
||||
<div class="user-card">
|
||||
<UserHeader />
|
||||
<UserText :user-text="userText" />
|
||||
<UserTabs v-model="activeTab" :tab-list="tabList" />
|
||||
|
||||
<el-form class="user-form" ref="registerFormRef" :model="registerForm" :rules="registerRules">
|
||||
|
||||
<Transition name="form-switch" mode="out-in">
|
||||
<el-form-item class="captcha-item" v-if="activeTab === 'email'" key="form-email-item" prop="email">
|
||||
<el-input v-model="registerForm.email" placeholder="请输入邮箱" clearable size="large" />
|
||||
<el-button type="primary" class="captcha-btn" @click="handleSendCode" :disabled="emailIsCounting">{{
|
||||
emailIsCounting ? `${emailCountdown}s后重新获取` : '发送验证码' }}</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item class="phone-item captcha-item" prop="phone" v-else-if="activeTab === 'phone'"
|
||||
key="form-phone-item">
|
||||
<el-select v-model="registerForm.countryCode" :teleported="false" value-key="code" placeholder="国家"
|
||||
size="large">
|
||||
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code">
|
||||
<div class="country-option">
|
||||
<span class="country-name">{{ item.name }}</span>
|
||||
<span class="country-code">{{ item.code }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input v-model="registerForm.phone" placeholder="请输入手机号" clearable size="large" />
|
||||
<el-button type="primary" class="captcha-btn" @click="handleSendCode" :disabled="phoneIsCounting">{{
|
||||
phoneIsCounting ? `${phoneCountdown}s后重新获取` : '发送验证码' }}</el-button>
|
||||
</el-form-item>
|
||||
</Transition>
|
||||
|
||||
<UserCaptcha v-model="registerForm.code" />
|
||||
<el-form-item label="设置密码(6-12位密码,包含特殊符号以及大小写)" label-position="top" prop="password">
|
||||
<el-input placeholder="请输入密码" clearable show-password size="large" type="password"
|
||||
v-model="registerForm.password" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邀请链接/邀请码(选填)" label-position="top" prop="inviteCode">
|
||||
<el-input placeholder="请输入邀请链接/邀请码" clearable size="large" type="text" v-model="registerForm.inviteCode" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox label="同意注册协议">我已阅读并同意 隐私条例、 退款政策、 AML&CTF政策、<br> 执行政策 和 网站条款和条件</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-submit">
|
||||
<el-button type="primary" class="form-btn" @click="handleRegister">注册</el-button>
|
||||
</el-form-item>
|
||||
<router-link to="/user/login" class="to-login">已有账号?立即登录</router-link>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// type类型
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
// 组件
|
||||
import UserHeader from './components/Header.vue'
|
||||
import UserText from './components/Text.vue'
|
||||
import UserTabs from './components/Tabs.vue'
|
||||
import UserCaptcha from './components/Captcha.vue'
|
||||
|
||||
//公共方法/store/配置文件
|
||||
import { useUserStore } from '@/stores/modules/user'
|
||||
import { md5Encrypt } from '@/utils/crypto'
|
||||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
|
||||
|
||||
//用户校验规则
|
||||
import { countryOptions } from '@/views/user/config/mock'
|
||||
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
|
||||
|
||||
//store实例
|
||||
const userStore = useUserStore()
|
||||
|
||||
// UserText 实例
|
||||
const userText = ref({
|
||||
backText: '',
|
||||
title: '注册',
|
||||
text: '',
|
||||
});
|
||||
|
||||
//tabs 实例
|
||||
const tabList = ref([
|
||||
{ label: '邮箱', name: 'email' },
|
||||
{ label: '手机', name: 'phone' },
|
||||
])
|
||||
const activeTab = ref<string>('email')
|
||||
|
||||
// 注册表单实例
|
||||
const registerFormRef = ref<FormInstance>()
|
||||
const registerForm = ref({
|
||||
email: '',
|
||||
countryCode: '+86',
|
||||
phone: '',
|
||||
password: '',
|
||||
code: '',
|
||||
inviteCode: ''
|
||||
})
|
||||
const registerRules = ref<FormRules>({
|
||||
email: emailRules().email,
|
||||
phone: phoneRules(registerForm.value).phone,
|
||||
password: passwordRules(registerForm.value).password,
|
||||
code: codeRules().code,
|
||||
})
|
||||
|
||||
//初始化验证码倒计时状态
|
||||
const emailCaptchaState = initCaptchaState('email'); // 邮箱专属
|
||||
const phoneCaptchaState = initCaptchaState('phone'); // 手机专属
|
||||
// 邮箱验证码状态
|
||||
const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState
|
||||
// 手机验证码状态
|
||||
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
|
||||
|
||||
|
||||
//方法
|
||||
// 定义发送验证码接口
|
||||
const sendCodeApi = async () => {
|
||||
// 发送验证码参数
|
||||
const sendParams = {
|
||||
email: registerForm.value.email,
|
||||
mobile: registerForm.value.phone,
|
||||
reg_type: activeTab.value === 'email' ? 1 : 2,
|
||||
type:'resetPass_code'
|
||||
}
|
||||
return userStore.sendCode(sendParams)
|
||||
}
|
||||
//发送验证码
|
||||
const handleSendCode = async () => {
|
||||
if (!registerFormRef.value) return
|
||||
try {
|
||||
await registerFormRef.value.validateField(activeTab.value)
|
||||
} catch (error) {
|
||||
console.error('字段校验失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
// 确定类型、接收方、对应的倒计时状态和发送函数
|
||||
const type: string = activeTab.value === 'phone' ? 'phone' : 'email';
|
||||
const receiver = type === 'phone' ? registerForm.value.phone : registerForm.value.email;
|
||||
const currentCaptchaState = type === 'phone' ? phoneCaptchaState : emailCaptchaState;
|
||||
const sendFunc = type === 'phone' ? "sendPhoneCaptcha" : "sendEmailCaptcha";
|
||||
|
||||
// 调用封装的发送函数(传正确的函数,而非字符串)
|
||||
await sendCaptcha(
|
||||
type,
|
||||
receiver,
|
||||
currentCaptchaState, // 只更新当前类型的倒计时状态
|
||||
() => sendCodeApi() // 传入返回Promise的函数
|
||||
);
|
||||
}
|
||||
|
||||
//注册
|
||||
const handleRegister = async () => {
|
||||
if (!registerFormRef.value) return;
|
||||
try {
|
||||
// 提交时再次验证(显示错误提示)
|
||||
await registerFormRef.value.validate();
|
||||
|
||||
// 注册参数
|
||||
const registerParams = {
|
||||
email: registerForm.value.email,
|
||||
mobile: registerForm.value.phone,
|
||||
reg_type: activeTab.value === 'email' ? 1 : 2,
|
||||
invite_code: registerForm.value.inviteCode,
|
||||
code: registerForm.value.code,
|
||||
password: md5Encrypt(registerForm.value.password)
|
||||
};
|
||||
|
||||
// 注册逻辑
|
||||
await userStore.register(registerParams);
|
||||
} catch (error) {
|
||||
console.error('表单校验失败:', error);
|
||||
}
|
||||
}
|
||||
// 组件卸载时清除定时器(关键:防止内存泄漏)
|
||||
onUnmounted(() => {
|
||||
clearCaptchaTimer(emailCaptchaState);
|
||||
clearCaptchaTimer(phoneCaptchaState);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-form {
|
||||
|
||||
.phone-item,
|
||||
.captcha-item {
|
||||
:deep(.el-form-item__content) {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
:deep(.el-select-dropdown__item) {
|
||||
padding: 0 16px 0 16px;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 180px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.to-login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
color: #409eff;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
<template>
|
||||
<el-form-item class="captcha-container" label="请输入验证码" label-position="top" :prop="prop">
|
||||
<el-input v-for="(item, index) in inputList" :key="index" v-model="inputList[index]" class="captcha-input-item"
|
||||
:maxlength="1" @input="handleInput(index, $event)" @focus="handleFocus(index)" @keyup.delete="handleDelete(index)"
|
||||
ref="inputRefs" type="text" />
|
||||
<el-button type="primary" class="form-btn" v-if="showSendButton" @click="sendCode" :disabled="sendState.isCounting">{{
|
||||
sendState.isCounting ? `${sendState.countdown}s后重新获取` : '发送验证码' }}</el-button>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
showSendButton: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
sendState: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
isCounting: false,
|
||||
countdown: 0,
|
||||
})
|
||||
},
|
||||
prop: {
|
||||
type: String,
|
||||
default: 'code' // 和父组件rules中的key一致
|
||||
},
|
||||
})
|
||||
const { showSendButton } = toRefs(props)
|
||||
const emit = defineEmits(['update:modelValue', 'change', 'finish', 'sendCode'])
|
||||
|
||||
const inputRefs = ref<Array<HTMLElement | null>>([]) // 补充 TS 类型
|
||||
const inputList = reactive<string[]>(new Array(props.length).fill(''))
|
||||
|
||||
// 修复:优化 modelValue 监听逻辑,避免首次清空输入
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal && newVal.length === props.length && newVal !== inputList.join('')) {
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
inputList[i] = newVal[i] || '';
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 修复:精准同步输入内容到 modelValue
|
||||
watch(
|
||||
() => [...inputList], // 深度监听数组变化
|
||||
(newList) => {
|
||||
const newVal = newList.join('');
|
||||
emit('update:modelValue', newVal);
|
||||
emit('change', newVal);
|
||||
if (newVal.length === props.length) {
|
||||
emit('finish', newVal);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 修复:输入处理逻辑,确保字符正确赋值
|
||||
const handleInput = (index: number, val: string) => {
|
||||
// 只保留数字,且只取第一个字符
|
||||
const cleanVal = val.replace(/\D/g, '').slice(0, 1);
|
||||
inputList[index] = cleanVal; // 确保赋值有效
|
||||
|
||||
// 自动跳转下一个输入框
|
||||
if (cleanVal && index < props.length - 1) {
|
||||
nextTick(() => {
|
||||
inputRefs.value[index + 1]?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleFocus = (index: number) => {
|
||||
// 聚焦时清空当前输入框(可选:如果不需要清空可注释)
|
||||
inputList[index] = '';
|
||||
}
|
||||
|
||||
// 修复:删除键逻辑,确保删除后同步值
|
||||
const handleDelete = (index: number) => {
|
||||
if (inputList[index] === '') {
|
||||
if (index > 0) {
|
||||
nextTick(() => {
|
||||
inputRefs.value[index - 1]?.focus();
|
||||
inputList[index - 1] = ''; // 清空上一个输入框
|
||||
});
|
||||
}
|
||||
} else {
|
||||
inputList[index] = '';
|
||||
}
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
inputList.fill('');
|
||||
emit('update:modelValue', '');
|
||||
nextTick(() => {
|
||||
inputRefs.value[0]?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({ clear })
|
||||
|
||||
// 发送验证码
|
||||
const sendCode = () => {
|
||||
emit('sendCode');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.captcha-container {
|
||||
:deep(.el-form-item__content) {
|
||||
flex-wrap: nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.captcha-input-item {
|
||||
:deep(.el-input__wrapper) {
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-btn {
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<!-- src/components/Header.vue -->
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="logo">
|
||||
<img src="@/assets/images/logo.webp" alt="" />
|
||||
</div>
|
||||
<LanguageSelect v-model="languageStore.lang" @change="languageStore.changeLang" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 导入UI组件
|
||||
import LanguageSelect from '@/components/LanguageSelect/index.vue'
|
||||
|
||||
// 导入Pinia状态
|
||||
import { useLanguageStore } from '@/stores/modules/language'
|
||||
|
||||
// 创建Pinia状态实例
|
||||
const languageStore = useLanguageStore()
|
||||
</script>
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<template>
|
||||
<el-tabs v-model="innerTabValue" class="user-tabs" @tab-change="handleTabChange"
|
||||
:class="hideTab ? 'hide-active' : ''">
|
||||
<el-tab-pane v-for="item in tabList" :key="item.name" :label="item.label" :name="item.name">
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TabPaneName } from 'element-plus'
|
||||
|
||||
// 1. TabItem 类型定义
|
||||
export interface TabItem {
|
||||
label: string;
|
||||
name: TabPaneName;
|
||||
}
|
||||
// 2. props 定义
|
||||
const props = defineProps<{
|
||||
tabList: TabItem[]; // 自定义选项卡列表
|
||||
modelValue: TabPaneName; // 父组件传递的选中值(必传)
|
||||
hideTab?: boolean; // 是否隐藏密码和验证码选项卡
|
||||
}>()
|
||||
|
||||
// 3. 定义 Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: TabPaneName): void;
|
||||
(e: 'tab-change', value: TabPaneName): void;
|
||||
}>()
|
||||
|
||||
// 4. 内部响应式值 + 监听父组件值变化
|
||||
const innerTabValue = ref<TabPaneName>(props.modelValue)
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
innerTabValue.value = newVal
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 5. Tab 切换通知父组件
|
||||
const handleTabChange = (newVal: TabPaneName) => {
|
||||
emit('update:modelValue', newVal)
|
||||
emit('tab-change', newVal)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-tabs {
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.hide-active {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<template>
|
||||
<div class="user-text">
|
||||
<el-button type="primary" :icon="ArrowLeft" class="back" v-if="userText.backText" @click="handleBack">{{ userText.backText
|
||||
}}</el-button>
|
||||
<h2 v-if="userText.title">{{ userText.title }}</h2>
|
||||
<p class="text" v-if="userText.text">{{ userText.text }}</p>
|
||||
<p class="account" v-if="userText.account">{{ userText.account }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
interface UserText {
|
||||
backText: string
|
||||
title: string
|
||||
text: string
|
||||
account: string
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
userText?: Partial<UserText>
|
||||
}>(),
|
||||
{
|
||||
userText: () => ({
|
||||
backText: '',
|
||||
title: '',
|
||||
text: '',
|
||||
account: ''
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits(['back'])
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-text {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.back {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #000;
|
||||
margin-bottom: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
//国家码选项:用于手机号登录
|
||||
interface CountryItem {
|
||||
name: string
|
||||
code: string
|
||||
}
|
||||
|
||||
export const countryOptions = ref<CountryItem[]>([
|
||||
{ name: '中国', code: '+86' },
|
||||
{ name: '中国台湾', code: '+886' },
|
||||
{ name: '中国香港', code: '+852' },
|
||||
{ name: '中国澳门', code: '+853' },
|
||||
{ name: '韩国', code: '+82' },
|
||||
{ name: '美国', code: '+1' },
|
||||
{ name: '新加坡', code: '+65' }
|
||||
])
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import type { FormRules } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 邮箱验证规则
|
||||
* @returns 邮箱校验规则
|
||||
*/
|
||||
export const emailRules = (): FormRules => {
|
||||
return {
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱地址', trigger: ['blur', 'change'] },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'change'] }
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 手机号验证规则(支持动态传入表单数据,适配国家码校验)
|
||||
* @param loginForm 表单数据(包含countryCode和phone)
|
||||
* @returns 手机号校验规则
|
||||
*/
|
||||
export const phoneRules = (loginForm: { countryCode: string; phone: string }): FormRules => {
|
||||
return {
|
||||
phone: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
// 验证国家码必填
|
||||
if (!loginForm.countryCode) {
|
||||
return callback(new Error('请选择国家/地区码'));
|
||||
}
|
||||
// 验证手机号必填
|
||||
if (!value) {
|
||||
return callback(new Error('请输入手机号码'));
|
||||
}
|
||||
// 中国大陆手机号格式验证(仅+86时生效)
|
||||
if (loginForm.countryCode === '+86' && !/^1[3-9]\d{9}$/.test(value)) {
|
||||
return callback(new Error('请输入正确的中国大陆手机号'));
|
||||
}
|
||||
callback();
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 密码验证规则(通用,后续可扩展)
|
||||
* @returns 密码校验规则
|
||||
*/
|
||||
// 传参改为:表单对象(响应式) + 密码字段名(默认password)
|
||||
export const passwordRules = (form: any, passwordField = 'password'): FormRules => {
|
||||
return {
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: ['blur', 'change'] },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: ['blur', 'change'] }
|
||||
],
|
||||
confirmPassword: [{
|
||||
required: true,
|
||||
message: '请确认密码',
|
||||
trigger: 'blur'
|
||||
}, {
|
||||
validator: (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error('请确认密码'));
|
||||
return;
|
||||
}
|
||||
// 实时读取表单对象的最新密码值(核心:form是响应式引用)
|
||||
if (value !== form[passwordField]) {
|
||||
callback(new Error('两次输入的密码不一致'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
export const codeRules = (length: number = 6): FormRules => {
|
||||
return {
|
||||
code: [
|
||||
// 1. 必填校验
|
||||
{
|
||||
required: true,
|
||||
message: '请输入验证码',
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
// 2. 长度校验(匹配组件的length属性)
|
||||
{
|
||||
min: length,
|
||||
max: length,
|
||||
message: `验证码必须输入${length}位`,
|
||||
trigger: ['change', 'blur']
|
||||
},
|
||||
// 3. 仅数字校验
|
||||
{
|
||||
pattern: /^\d+$/,
|
||||
message: '验证码只能输入数字',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<template>
|
||||
<div class="user-container">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="user" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"extends": "@tsconfig/node24/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// 自动导入插件
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
|
||||
// Element Plus 组件自动导入
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
//自动导入Vue/路由/Pinia等API(无需手动import)
|
||||
AutoImport({
|
||||
// 适配Element Plus的自动导入
|
||||
resolvers: [
|
||||
ElementPlusResolver(),
|
||||
],
|
||||
// 指定要自动导入的库
|
||||
imports: [
|
||||
'vue', // 自动导入ref、reactive、watch等Vue API
|
||||
'vue-router', // 自动导入useRoute、useRouter等路由API
|
||||
'pinia', // 自动导入defineStore等Pinia API
|
||||
'@vueuse/core', // 自动导入@vueuse的hooks
|
||||
],
|
||||
// 生成类型声明文件(TypeScript必备,解决类型提示问题)
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
// 自动生成eslint配置(解决eslint报"未定义变量"的问题)
|
||||
eslintrc: {
|
||||
enabled: true, // 开启后会生成 .eslintrc-auto-import.json
|
||||
},
|
||||
}),
|
||||
// 2. 自动导入组件(包括Element Plus和本地组件)
|
||||
Components({
|
||||
// 自动解析Element Plus组件
|
||||
resolvers: [
|
||||
ElementPlusResolver(),
|
||||
],
|
||||
// 生成组件类型声明文件
|
||||
dts: 'src/components.d.ts',
|
||||
// 指定要自动导入的本地组件目录(src/components下的组件无需手动注册)
|
||||
dirs: ['src/components'],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
|
||||
// 构建配置
|
||||
build: {
|
||||
// 输出目录
|
||||
outDir: 'dist',
|
||||
// 静态资源目录
|
||||
assetsDir: 'assets',
|
||||
// 生产环境移除console和debugger
|
||||
minify: 'esbuild',
|
||||
esbuild: {
|
||||
drop: mode === 'production' ? ['console', 'debugger'] : [],
|
||||
// 剔除 JS 中的所有注释(包括注释掉的代码)
|
||||
minifyIdentifiers: true, // 压缩变量名
|
||||
minifySyntax: true, // 压缩语法(含剔除普通注释)
|
||||
minifyWhitespace: true, // 剔除空格
|
||||
},
|
||||
// 可选:进一步压缩打包产物
|
||||
// terserOptions: mode === 'production' ? {
|
||||
// format: {
|
||||
// comments: false, // 彻底剔除所有注释
|
||||
// },
|
||||
// } : {},
|
||||
},
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue