111
This commit is contained in:
672
Web/docs/remand.md
Normal file
672
Web/docs/remand.md
Normal file
@@ -0,0 +1,672 @@
|
||||
# Nuxt4 + Pinia 前端规范化框架设计规范(终极完整版)
|
||||
|
||||
# 一、文档概述
|
||||
|
||||
## 1.1 文档基本信息
|
||||
|
||||
|项目|说明|
|
||||
|---|---|
|
||||
|文档版本|V1.0|
|
||||
|适用场景|企业级Web应用、IM即时通讯系统、模块化前端项目、需自定义工具库及API服务的项目|
|
||||
|核心技术栈|Nuxt4、Vue3、TypeScript、Pinia|
|
||||
|设计原则|规范化、模块化、高可维护、单向数据流、工具与API统一管控、可扩展|
|
||||
|文档用途|作为项目开发规范基准,统一团队开发标准,指导架构落地、后期迭代与扩展|
|
||||
## 1.2 设计目标
|
||||
|
||||
- 实现前端全局状态中心化、规范化管理,杜绝零散状态污染,确保状态变更可追溯、可管控,对齐后端分层架构思维。
|
||||
|
||||
- 自定义工具库统一命名、集中归集,支持全局自动挂载,无需手动导入,提升开发效率,降低代码冗余。
|
||||
|
||||
- 工具层支持静态方法调用、new实例化调用两种形态,适配不同业务场景(无状态工具/有状态工具)。
|
||||
|
||||
- API服务统一封装、集中管理,支持全局自动导入,可直接实例化调用,简化接口请求流程,统一请求/响应处理逻辑。
|
||||
|
||||
- 保障全局长连接(如IM)、登录态稳定,适配后期插件化扩展(多插件商城、用户体系等),确保架构可扩展、不崩解。
|
||||
|
||||
- 实现前后端开发规范对齐,降低跨端协作成本,前期立死规矩,后期开发更快、更不乱。
|
||||
|
||||
# 二、整体架构设计
|
||||
|
||||
## 2.1 架构核心理念
|
||||
|
||||
采用「分层治理 + 模块化拆分」架构,严格遵循单向数据流原则,实现视图、业务逻辑、状态、工具层、API服务层完全解耦;全局状态由Pinia统一管控,自定义工具与API服务统一规约归集,所有公共模块(Stores、Composables、EXTEND工具、API服务)自动注入,简化开发流程,强化团队开发规范一致性,避免代码冗余与混乱,确保项目可维护、可扩展。
|
||||
|
||||
## 2.2 架构分层拓扑
|
||||
|
||||
```plain text
|
||||
浏览器客户端
|
||||
↓
|
||||
视图渲染层(Layout布局 + Page页面 + 公共组件)
|
||||
↓
|
||||
业务逻辑层(Composables组合式函数)
|
||||
↓
|
||||
全局工具层(EXTEND结尾自定义工具库)
|
||||
↓
|
||||
API服务层(SERVICE结尾接口服务库)
|
||||
↓
|
||||
状态管理层(Pinia模块化仓库)
|
||||
↓
|
||||
数据请求层(API接口、长连接服务)
|
||||
↓
|
||||
后端服务 + 本地持久化缓存(兜底)
|
||||
```
|
||||
|
||||
## 2.3 工程目录规范(src源码模式)
|
||||
|
||||
遵循Nuxt4官方推荐的src目录结构,结合项目需求定制,新增API服务专属目录,确保目录清晰、职责明确,所有核心模块集中归集,便于维护与扩展:
|
||||
|
||||
```plain text
|
||||
src/
|
||||
├── app.vue # 项目根入口文件,全局样式、全局挂载入口
|
||||
├── layouts/ # 全局布局模板,路由切换不销毁(核心常驻组件载体)
|
||||
│ ├── default.vue # 默认主布局(承载导航栏、页脚、IM常驻悬浮组件)
|
||||
│ └── empty.vue # 空白布局(登录页、弹窗、独立页面专用)
|
||||
├── pages/ # 约定式路由页面,按路由路径组织
|
||||
│ ├── index.vue # 首页
|
||||
│ ├── login.vue # 登录页
|
||||
│ └── im/ # IM模块页面
|
||||
│ ├── chat.vue # 聊天页面
|
||||
│ └── contact.vue # 联系人页面
|
||||
├── components/ # 全局公共组件,自动导入,按功能分类
|
||||
│ ├── ui/ # 基础UI组件(Button、Card、Input等)
|
||||
│ └── business/ # 业务组件(IM消息气泡、用户卡片等)
|
||||
├── stores/ # Pinia状态仓库,模块化拆分,自动导入
|
||||
│ ├── user.ts # 用户登录状态仓库(登录信息、Token等)
|
||||
│ ├── im.ts # IM聊天状态仓库(未读总数、会话列表等)
|
||||
│ └── app.ts # 全局应用状态仓库(主题、加载态等)
|
||||
├── composables/ # 业务组合式函数,自动导入,封装复用逻辑
|
||||
│ ├── useAuth.ts # 权限校验逻辑
|
||||
│ └── useImSocket.ts # IM长连接逻辑(SignalR/WebSocket封装)
|
||||
├── extends/ # 【核心】自定义工具库专属目录,统一归集
|
||||
│ ├── dateEXTEND.ts # 日期处理工具(格式化、日期差等)
|
||||
│ ├── cryptoEXTEND.ts # 加密解密工具(对称加密、Base64等)
|
||||
│ ├── requestEXTEND.ts # 网络请求工具(Axios/ofetch封装、拦截器等)
|
||||
│ └── imHelperEXTEND.ts # IM业务辅助工具(消息格式化、会话处理等)
|
||||
├── services/ # 【新增核心】API服务专属目录,统一归集,自动导入
|
||||
│ ├── userSERVICE.ts # 用户相关API服务(登录、获取用户信息等)
|
||||
│ ├── imSERVICE.ts # IM相关API服务(获取会话、发送消息等)
|
||||
│ └── commonSERVICE.ts # 通用API服务(字典查询、文件上传等)
|
||||
├── types/ # 全局TypeScript类型定义,统一收口
|
||||
│ ├── user.ts # 用户相关类型(IUserInfo等)
|
||||
│ ├── im.ts # IM相关类型(IMessage、ISession等)
|
||||
│ ├── api.ts # API服务相关类型(请求参数、响应体等)
|
||||
│ └── common.ts # 通用类型(分页、响应体等)
|
||||
├── utils/ # 底层基础工具函数(无业务逻辑,纯原子方法)
|
||||
│ ├── base64.ts # 基础Base64工具(备用,优先用EXTEND工具)
|
||||
│ └── regex.ts # 正则校验工具(备用,优先用EXTEND工具)
|
||||
└── server/ # Nuxt服务端逻辑(SSR接口、中间件等)
|
||||
├── api/ # 服务端接口
|
||||
└── middleware/ # 服务端中间件(权限校验等)
|
||||
```
|
||||
|
||||
# 三、命名规范(强制遵守)
|
||||
|
||||
## 3.1 文件命名规范
|
||||
|
||||
- 页面/组件文件:采用大驼峰(PascalCase),语义化清晰,如`UserLogin.vue`、`ImChat.vue`;组件按功能分类存放(ui/业务),避免杂乱。
|
||||
|
||||
- Pinia状态仓库文件:采用小驼峰(camelCase),以业务域命名,如`user.ts`、`im.ts`,禁止以业务无关名称命名。
|
||||
|
||||
- 自定义工具库文件:**固定格式「功能名+EXTEND.ts」,必须以EXTEND结尾,无多余符号**,如`dateEXTEND.ts`、`validateEXTEND.ts`、`imHelperEXTEND.ts`,统一放在`src/extends/`目录下。
|
||||
|
||||
- API服务文件:**固定格式「功能名+SERVICE.ts」,必须以SERVICE结尾,无多余符号**,如`userSERVICE.ts`、`imSERVICE.ts`,统一放在`src/services/`目录下。
|
||||
|
||||
- 类型文件:采用小驼峰(camelCase),按业务分类,如`user.ts`、`common.ts`、`api.ts`,类型定义统一收口,避免重复定义。
|
||||
|
||||
- Composables函数文件:采用小驼峰(camelCase),前缀统一加`use`,如`useAuth.ts`、`useImSocket.ts`,符合Nuxt自动导入约定。
|
||||
|
||||
## 3.2 变量/函数/类命名规范
|
||||
|
||||
- 变量/函数:采用小驼峰(camelCase),语义化清晰,禁止使用拼音、简写(通用简写除外,如`uid`),如`loginUser`、`formatTime`。
|
||||
|
||||
- 类/接口:采用大驼峰(PascalCase),接口前缀加`I`,如`IUserInfo`、`IMessage`;工具类名称与文件名一致,以EXTEND结尾,如`DateEXTEND`、`CryptoEXTEND`;API服务类名称与文件名一致,以SERVICE结尾,如`UserSERVICE`、`ImSERVICE`。
|
||||
|
||||
- 常量:采用全大写(UPPER_CASE),下划线分隔,如`MAX_PAGE_SIZE`、`TOKEN_KEY`,统一放在对应工具、API服务或类型文件中。
|
||||
|
||||
# 四、Pinia状态管理规范(核心规范)
|
||||
|
||||
## 4.1 核心设计原则
|
||||
|
||||
- 单一职责原则:一个仓库只管理一个业务域的状态,禁止跨业务域冗余数据(如用户状态与IM状态分开管理),确保模块解耦。
|
||||
|
||||
- 禁止直接修改原则:state为私有状态,**严禁外部直接修改state值**,所有状态变更必须通过actions方法,确保状态变更可追溯、可管控,避免脏数据。
|
||||
|
||||
- 计算派生原则:getters作为只读计算属性,封装状态判断、数据格式化、派生逻辑,禁止在组件中重复编写计算逻辑,提升代码复用性。
|
||||
|
||||
- 持久化管控原则:仅核心状态(登录Token、用户基础信息)开启持久化,禁止大量数据(如会话列表、消息记录)存入本地,避免占用本地存储、影响性能。
|
||||
|
||||
- 自动导入原则:stores目录下所有仓库自动全局导入,无需手动编写import语句,直接调用即可,简化开发流程。
|
||||
|
||||
- 类型安全原则:所有状态、方法均需指定TypeScript类型,避免any类型,确保代码健壮性,降低后期维护成本。
|
||||
|
||||
## 4.2 依赖安装与配置
|
||||
|
||||
安装Pinia及持久化插件,确保状态持久化功能正常,适配Nuxt4自动导入(同步新增services目录自动导入配置):
|
||||
|
||||
```bash
|
||||
# 安装Pinia及Nuxt适配模块
|
||||
npm install pinia @pinia/nuxt
|
||||
|
||||
# 安装持久化插件(用于登录态等核心状态兜底)
|
||||
npm install @pinia-plugin-persistedstate/nuxt
|
||||
```
|
||||
|
||||
在`nuxt.config.ts`中配置,启用Pinia、自动导入Stores、Composables、EXTEND工具、API服务:
|
||||
|
||||
```typescript
|
||||
export default defineNuxtConfig({
|
||||
srcDir: 'src/', // 指定源码目录
|
||||
modules: [
|
||||
'@pinia/nuxt', // Pinia Nuxt适配模块
|
||||
'@pinia-plugin-persistedstate/nuxt' // 持久化插件
|
||||
],
|
||||
imports: {
|
||||
dirs: [
|
||||
'stores', // 自动扫描Pinia仓库,全局自动导入
|
||||
'composables', // 自动扫描组合式函数
|
||||
'extends', // 自动扫描EXTEND工具库,全局自动导入
|
||||
'services' // 【新增】自动扫描SERVICE API服务,全局自动导入
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 4.3 标准仓库编写格式(可直接复制使用)
|
||||
|
||||
```typescript
|
||||
// src/stores/user.ts(用户状态仓库示例)
|
||||
import { defineStore } from 'pinia'
|
||||
import type { IUserInfo } from '~/types/user'
|
||||
|
||||
// 仓库命名规范:use+业务域+Store(小驼峰),与文件名对应
|
||||
export const useUserStore = defineStore('user', {
|
||||
// 1. 原始状态:仅存基础数据,不做任何计算、判断,类型明确
|
||||
state: () => ({
|
||||
userInfo: null as IUserInfo | null, // 用户基础信息
|
||||
token: '' as string, // 登录Token
|
||||
isLoading: false as boolean // 登录加载态
|
||||
}),
|
||||
|
||||
// 2. 只读计算属性:封装派生逻辑,全局只读,自动缓存,避免重复计算
|
||||
getters: {
|
||||
// 判断是否登录(核心派生逻辑,统一封装)
|
||||
isLogin: (state) => !!state.token,
|
||||
// 获取用户ID(默认值兜底,避免报错)
|
||||
userId: (state) => state.userInfo?.id ?? 0,
|
||||
// 格式化用户昵称(派生逻辑,组件直接使用)
|
||||
userNickname: (state) => state.userInfo?.nickname || '未知用户'
|
||||
},
|
||||
|
||||
// 3. 唯一状态修改入口:所有状态变更必须走actions,可添加日志、校验、拦截
|
||||
actions: {
|
||||
// 登录成功:设置用户信息与Token
|
||||
setUserInfo(data: IUserInfo, token: string) {
|
||||
this.userInfo = data
|
||||
this.token = token
|
||||
this.isLoading = false
|
||||
},
|
||||
// 退出登录:清空用户状态
|
||||
clearUserInfo() {
|
||||
this.userInfo = null
|
||||
this.token = ''
|
||||
this.isLoading = false
|
||||
},
|
||||
// 设置登录加载态
|
||||
setLoginLoading(loading: boolean) {
|
||||
this.isLoading = loading
|
||||
}
|
||||
},
|
||||
|
||||
// 4. 本地持久化配置:仅缓存核心状态,刷新页面不丢失,避免大量数据存储
|
||||
persist: {
|
||||
enabled: true, // 开启持久化
|
||||
strategies: [
|
||||
{
|
||||
key: 'user-auth-store', // 本地存储key,避免与其他存储冲突
|
||||
storage: localStorage, // 存储介质(localStorage,前端本地持久化)
|
||||
paths: ['token', 'userInfo'] // 仅持久化指定字段,减少存储开销
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 4.4 状态使用规范
|
||||
|
||||
- 获取状态:优先使用getters获取派生数据,禁止直接读取state(如优先用`userStore.isLogin`,而非`userStore.state.token`),确保逻辑统一。
|
||||
|
||||
- 修改状态:必须调用actions方法,禁止直接赋值修改(如`userStore.setUserInfo(data, token)`,禁止`userStore.userInfo = data`),确保状态变更可追溯。
|
||||
|
||||
- 跨模块通信:不同仓库之间通过引入对应仓库、调用其actions方法实现通信,禁止直接操作其他仓库的state,避免模块耦合。
|
||||
|
||||
- 生命周期:Pinia状态常驻浏览器内存,路由切换、组件销毁时不丢失;刷新页面时,通过persist配置恢复核心状态(如登录态),非核心状态重新初始化。
|
||||
|
||||
- 性能优化:Pinia的getters自带缓存特性,重复调用不重复计算;对于复杂状态,可使用shallowRef处理,降低响应式开销。
|
||||
|
||||
# 五、EXTEND自定义工具库规范(核心定制规范)
|
||||
|
||||
## 5.1 核心规约(强制遵守)
|
||||
|
||||
- 目录归集:所有自定义工具库必须放在`src/extends/`目录下,禁止散落至其他目录(如utils、composables),确保工具层统一管控,便于查找与维护。
|
||||
|
||||
- 命名规范:工具文件名固定为「功能名+EXTEND.ts」,必须以EXTEND结尾,工具类名称与文件名一致(如文件`dateEXTEND.ts`,类名`DateEXTEND`),禁止随意命名。
|
||||
|
||||
- 自动挂载:通过Nuxt配置实现extends目录全局自动扫描、自动导入,页面/组件/仓库/API服务中无需手动import,直接使用,提升开发效率。
|
||||
|
||||
- 职责边界:工具层仅封装纯逻辑、通用方法,不涉及业务状态修改,不直接操作Pinia仓库;业务相关逻辑下沉至composables,确保工具层复用性。
|
||||
|
||||
- 调用形态:支持两种编写与调用形态,可根据业务场景自由选择(无状态工具用静态类,有状态工具用实例化类),兼顾灵活性与规范性。
|
||||
|
||||
- 类型安全:工具方法、参数、返回值均需指定TypeScript类型,禁止any类型,确保代码健壮性,便于团队协作。
|
||||
|
||||
## 5.2 工具编写与调用形态(可直接复制使用)
|
||||
|
||||
### 形态一:静态类工具(无需new,直接调用)
|
||||
|
||||
适合无实例属性、纯工具方法场景(如日期格式化、加密解密、正则校验),无需创建实例,直接通过类名调用方法,简洁高效,全局统一调用入口。
|
||||
|
||||
```typescript
|
||||
// src/extends/dateEXTEND.ts(日期工具示例,静态类)
|
||||
export class DateEXTEND {
|
||||
/**
|
||||
* 格式化时间戳为本地时间字符串
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns 格式化后的时间字符串(如:2026-04-09 23:59:59)
|
||||
*/
|
||||
static format(timestamp: number): string {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
const second = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个时间戳的天数差
|
||||
* @param start 开始时间戳
|
||||
* @param end 结束时间戳
|
||||
* @returns 天数差(正数:end在start之后;负数:end在start之前)
|
||||
*/
|
||||
static diffDay(start: number, end: number): number {
|
||||
const oneDay = 1000 * 60 * 60 * 24
|
||||
return Math.floor((end - start) / oneDay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为今天
|
||||
* @param timestamp 时间戳(毫秒)
|
||||
* @returns boolean 是今天返回true,否则返回false
|
||||
*/
|
||||
static isToday(timestamp: number): boolean {
|
||||
const today = new Date()
|
||||
const target = new Date(timestamp)
|
||||
return (
|
||||
today.getFullYear() === target.getFullYear() &&
|
||||
today.getMonth() === target.getMonth() &&
|
||||
today.getDate() === target.getDate()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用方式(无需import,全局直接使用)
|
||||
// const time = DateEXTEND.format(Date.now())
|
||||
// const dayDiff = DateEXTEND.diffDay(startTime, endTime)
|
||||
// const isToday = DateEXTEND.isToday(timestamp)
|
||||
```
|
||||
|
||||
### 形态二:实例化类工具(支持new创建实例)
|
||||
|
||||
适合需要独立实例、携带私有属性场景(如加密解密、请求拦截、多实例独立上下文),通过new创建实例,每个实例拥有独立的私有属性,避免全局污染。
|
||||
|
||||
```typescript
|
||||
// src/extends/cryptoEXTEND.ts(加密工具示例,可实例化)
|
||||
export class CryptoEXTEND {
|
||||
// 私有属性:加密密钥,每个实例独立拥有
|
||||
private key: string
|
||||
|
||||
/**
|
||||
* 构造函数:初始化加密密钥
|
||||
* @param key 加密密钥(不同实例可传入不同密钥)
|
||||
*/
|
||||
constructor(key: string) {
|
||||
this.key = key
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密字符串(Base64 + 密钥拼接)
|
||||
* @param data 需要加密的字符串
|
||||
* @returns 加密后的字符串
|
||||
*/
|
||||
encrypt(data: string): string {
|
||||
// 加密逻辑:拼接密钥后进行Base64编码
|
||||
const encryptStr = data + this.key
|
||||
return btoa(encodeURIComponent(encryptStr))
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密字符串
|
||||
* @param data 需要解密的字符串
|
||||
* @returns 解密后的原始字符串
|
||||
*/
|
||||
decrypt(data: string): string {
|
||||
// 解密逻辑:Base64解码后移除密钥
|
||||
const decryptStr = decodeURIComponent(atob(data))
|
||||
return decryptStr.replace(this.key, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证加密字符串是否有效(匹配当前密钥)
|
||||
* @param data 加密后的字符串
|
||||
* @returns boolean 有效返回true,否则返回false
|
||||
*/
|
||||
validate(data: string): boolean {
|
||||
try {
|
||||
const decryptStr = this.decrypt(data)
|
||||
return decryptStr !== data // 解密后与原始加密串不一致,说明有效
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用方式(无需import,全局直接new实例使用)
|
||||
// const crypto1 = new CryptoEXTEND('custom-key-1')
|
||||
// const encryptData1 = crypto1.encrypt('test-data-1')
|
||||
// const decryptData1 = crypto1.decrypt(encryptData1)
|
||||
|
||||
// const crypto2 = new CryptoEXTEND('custom-key-2') // 不同密钥的实例
|
||||
// const encryptData2 = crypto2.encrypt('test-data-2')
|
||||
```
|
||||
|
||||
## 5.3 工具库使用注意事项
|
||||
|
||||
- 工具方法需保证纯函数特性(无副作用),输入相同参数,输出结果一致,便于复用与测试。
|
||||
|
||||
- 避免工具库之间相互依赖,若需依赖,需明确依赖关系,避免循环依赖。
|
||||
|
||||
- 工具库中禁止直接操作DOM、Pinia状态,所有业务相关操作需通过composables中转。
|
||||
|
||||
- 常用工具(如日期、加密)优先使用EXTEND工具库,禁止在组件中重复编写相同逻辑,提升代码复用性。
|
||||
|
||||
- API服务中可直接调用EXTEND工具(如请求拦截中使用加密工具),实现逻辑复用。
|
||||
|
||||
# 六、SERVICE API服务规范(新增核心规范)
|
||||
|
||||
## 6.1 核心规约(强制遵守)
|
||||
|
||||
- 目录归集:所有API服务必须放在`src/services/`目录下,禁止散落至其他目录(如pages、composables),确保API服务统一管控,便于维护、调试与迭代。
|
||||
|
||||
- 命名规范:API服务文件名固定为「功能名+SERVICE.ts」,必须以SERVICE结尾,服务类名称与文件名一致(如文件`userSERVICE.ts`,类名`UserSERVICE`),禁止随意命名。
|
||||
|
||||
- 自动挂载:通过Nuxt配置实现services目录全局自动扫描、自动导入,页面/组件/仓库中无需手动import,直接实例化调用,简化接口请求流程。
|
||||
|
||||
- 职责边界:API服务仅封装接口请求逻辑(请求参数处理、接口调用、响应数据格式化),不涉及业务逻辑、状态修改;业务逻辑下沉至composables,状态修改通过Pinia actions实现。
|
||||
|
||||
- 调用形态:统一采用「实例化调用」,通过new创建服务实例,每个实例可独立配置请求参数、拦截逻辑,适配多场景接口调用(如不同模块的请求头差异)。
|
||||
|
||||
- 类型安全:所有API请求参数、响应数据均需指定TypeScript类型(统一在`types/api.ts`中定义),禁止any类型,确保接口调用的健壮性,减少类型错误。
|
||||
|
||||
- 请求统一:所有API请求必须通过`extends/requestEXTEND.ts`封装的请求工具,禁止直接使用ofetch/axios调用接口,确保请求拦截、响应拦截、异常处理统一。
|
||||
|
||||
## 6.2 API服务编写与调用形态(可直接复制使用)
|
||||
|
||||
统一采用实例化类编写,支持构造函数传入自定义配置(如请求头、超时时间),适配不同业务场景,调用时直接new实例,无需手动导入,符合你的预期调用方式(`const userSerive = new UserSerivce(); var userInfo = userSerive.getinfo();`)。
|
||||
|
||||
```typescript
|
||||
// 第一步:先在types/api.ts中定义请求/响应类型
|
||||
// src/types/api.ts
|
||||
import type { IUserInfo } from './user'
|
||||
|
||||
// 用户登录请求参数
|
||||
export interface ILoginParams {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
// 用户登录响应体
|
||||
export interface ILoginResponse {
|
||||
code: number
|
||||
message: string
|
||||
data: {
|
||||
token: string
|
||||
userInfo: IUserInfo
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息响应体
|
||||
export interface IGetUserInfoResponse {
|
||||
code: number
|
||||
message: string
|
||||
data: IUserInfo
|
||||
}
|
||||
|
||||
// 第二步:编写API服务(src/services/userSERVICE.ts)
|
||||
import type { ILoginParams, ILoginResponse, IGetUserInfoResponse } from '~/types/api'
|
||||
import { RequestEXTEND } from '~/extends/requestEXTEND' // 引入统一请求工具
|
||||
|
||||
export class UserSERVICE {
|
||||
// 私有属性:请求工具实例(可自定义配置)
|
||||
private request: RequestEXTEND
|
||||
|
||||
/**
|
||||
* 构造函数:初始化请求工具,可传入自定义配置
|
||||
* @param config 自定义请求配置(可选,如超时时间、请求头)
|
||||
*/
|
||||
constructor(config?: { timeout?: number; headers?: Record<string, string> }) {
|
||||
// 初始化请求工具,传入自定义配置(默认使用全局配置)
|
||||
this.request = new RequestEXTEND(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录接口
|
||||
* @param params 登录请求参数
|
||||
* @returns 登录响应数据(格式化后)
|
||||
*/
|
||||
async login(params: ILoginParams): Promise<ILoginResponse['data']> {
|
||||
try {
|
||||
const response = await this.request.post<ILoginResponse>('/api/user/login', params)
|
||||
// 统一响应处理:判断状态码,抛出异常或返回数据
|
||||
if (response.code !== 200) {
|
||||
throw new Error(response.message || '登录失败')
|
||||
}
|
||||
return response.data // 直接返回核心数据,简化组件调用
|
||||
} catch (error) {
|
||||
// 统一异常处理:可结合全局提示工具抛出异常
|
||||
console.error('登录接口异常:', error)
|
||||
throw error // 抛出异常,由调用方处理业务逻辑
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息接口(需携带Token)
|
||||
* @param userId 用户ID(可选,默认取当前登录用户ID)
|
||||
* @returns 用户信息
|
||||
*/
|
||||
async getInfo(userId?: number): Promise<IUserInfo> {
|
||||
try {
|
||||
const response = await this.request.get<IGetUserInfoResponse>('/api/user/info', {
|
||||
params: { userId } // 拼接请求参数
|
||||
})
|
||||
if (response.code !== 200) {
|
||||
throw new Error(response.message || '获取用户信息失败')
|
||||
}
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('获取用户信息接口异常:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录接口
|
||||
* @returns 退出结果
|
||||
*/
|
||||
async logout(): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.request.post<{ code: number; message: string }>('/api/user/logout')
|
||||
return response.code === 200
|
||||
} catch (error) {
|
||||
console.error('退出登录接口异常:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用方式(无需import,全局直接new实例使用,完全匹配你的需求)
|
||||
// 1. 基础调用(使用默认配置)
|
||||
// const userService = new UserSERVICE();
|
||||
// const userInfo = await userService.getInfo(); // 获取当前用户信息
|
||||
// const loginData = await userService.login({ username: 'admin', password: '123456' }); // 登录
|
||||
|
||||
// 2. 自定义配置调用(如设置超时时间、额外请求头)
|
||||
// const userService = new UserSERVICE({
|
||||
// timeout: 10000,
|
||||
// headers: { 'X-Custom-Header': 'custom-value' }
|
||||
// });
|
||||
// const userInfo = await userService.getInfo(1001); // 传入用户ID查询指定用户
|
||||
```
|
||||
|
||||
## 6.3 API服务使用注意事项
|
||||
|
||||
- API服务方法统一使用async/await语法,返回Promise对象,便于组件中异步调用、处理加载态。
|
||||
|
||||
- 所有接口请求必须通过`RequestEXTEND`工具,统一处理请求拦截(如添加Token、请求头)、响应拦截(如统一错误提示、Token过期处理)。
|
||||
|
||||
- API服务中禁止直接操作Pinia状态,若需修改状态(如登录后设置Token),需在组件/composables中调用API服务后,通过Pinia actions修改。
|
||||
|
||||
- 相同业务域的API接口统一放在一个SERVICE文件中,如用户相关接口都放在`userSERVICE.ts`,避免分散。
|
||||
|
||||
- 接口参数、响应体类型必须在`types/api.ts`中统一定义,禁止在服务中重复定义类型,确保类型统一。
|
||||
|
||||
- 复杂接口(如分页查询)可封装通用方法,传入分页参数,返回格式化后的分页数据,简化组件调用。
|
||||
|
||||
# 七、存储分层与性能规范
|
||||
|
||||
## 7.1 存储层级划分
|
||||
|
||||
采用「内存为主、磁盘兜底」的存储分层策略,兼顾性能与稳定性,避免性能瓶颈:
|
||||
|
||||
- 「内存存储(Pinia State)」:全局状态运行时载体,纳秒级读写,性能最优,业务核心读写优先使用。生命周期为页面运行期间,路由切换、组件销毁不丢失,刷新页面后非持久化状态清空。
|
||||
|
||||
- 「本地持久化(LocalStorage)」:仅作为Pinia持久化备份,用于存储核心状态(如登录Token、用户基础信息),刷新/重开页面可恢复核心状态。禁止直接操作LocalStorage,统一由Pinia持久化插件托管。
|
||||
|
||||
- 「禁止项」:禁止业务代码直接读写LocalStorage;禁止大量列表、非核心数据(如会话列表、消息记录)持久化,避免占用本地存储、影响首屏加载速度与性能。
|
||||
|
||||
## 7.2 性能管控规范
|
||||
|
||||
- Pinia性能:getters具备缓存特性,重复调用不重复计算;避免在state中存储大量数据,拆分模块化仓库,降低单个仓库体积。
|
||||
|
||||
- 工具库性能:工具方法纯函数封装,避免冗余逻辑;对于计算密集型任务,可考虑使用Web Worker,避免阻塞主线程。
|
||||
|
||||
- API服务性能:统一使用请求拦截、响应拦截,添加防抖节流,避免重复请求、无效请求;大文件上传/下载可封装单独方法,支持断点续传。
|
||||
|
||||
- 组件性能:全局常驻组件(如IM悬浮窗)放在Layout布局中,避免路由切换时重复创建/销毁;组件懒加载(Lazy前缀),降低首屏加载压力。
|
||||
|
||||
- 请求性能:网络请求统一封装在EXTEND工具中,添加请求拦截、响应拦截、防抖节流,避免重复请求、无效请求。
|
||||
|
||||
# 八、全局数据流规范
|
||||
|
||||
## 8.1 单向数据流原则(强制遵守)
|
||||
|
||||
严格遵循「View → Action → API Service/EXTEND → State → View」的单向闭环数据流,确保数据流可追溯、可管控,避免数据混乱:
|
||||
|
||||
1. View(页面/组件):触发用户交互(如登录、发送消息)。
|
||||
|
||||
2. Action(调用Store Actions / Composables函数):处理业务逻辑,调用API服务/EXTEND工具请求数据。
|
||||
|
||||
3. API Service/EXTEND:API服务调用后端接口获取数据,EXTEND工具处理数据逻辑,返回处理后的数据。
|
||||
|
||||
4. State(Pinia仓库):通过Actions更新内存状态,持久化状态自动同步至LocalStorage。
|
||||
|
||||
5. View(页面/组件):通过Getters获取状态,自动响应式更新视图。
|
||||
|
||||
## 8.2 通信规范
|
||||
|
||||
- 父子组件通信:使用Props/Emits,禁止使用全局状态传递简单父子组件数据。
|
||||
|
||||
- 兄弟/跨级组件通信:统一使用Pinia Store,通过修改/读取状态实现通信,禁止使用EventBus、全局变量。
|
||||
|
||||
- 页面与布局通信:页面(Page)与布局(Layout)之间的通信,必须通过Pinia Store,确保通信规范、可追溯。
|
||||
|
||||
- 长连接通信:IM长连接(SignalR/WebSocket)统一封装在Composables中,通过Pinia Store管理连接状态、消息数据,避免连接混乱。
|
||||
|
||||
- API服务与Pinia通信:API服务仅返回数据,不直接操作Pinia状态,由调用方(组件/composables)通过Pinia actions更新状态。
|
||||
|
||||
# 九、开发强制约束(必须遵守)
|
||||
|
||||
- 全局状态统一使用Pinia,禁止使用useState做全局状态共享,禁止使用全局变量、EventBus,避免状态污染。
|
||||
|
||||
- 自定义工具必须放在extends目录,文件名以EXTEND结尾,禁止散落其他目录,禁止重复编写相同工具方法。
|
||||
|
||||
- API服务必须放在services目录,文件名以SERVICE结尾,禁止散落其他目录,禁止在组件中直接调用接口,必须通过API服务。
|
||||
|
||||
- 严禁直接修改Pinia state,所有状态变更必须通过actions,可添加日志、校验、拦截,确保状态变更可追溯。
|
||||
|
||||
- 页面与布局分离,全局常驻组件(IM、导航)必须放在layouts/default.vue中,保证长连接不中断、组件不重复销毁/创建。
|
||||
|
||||
- 业务逻辑下沉至composables和EXTEND工具,页面组件只做视图渲染,禁止在页面中编写复杂业务逻辑、工具方法、接口调用。
|
||||
|
||||
- 所有接口请求统一封装在API服务中,API服务统一使用RequestEXTEND工具,禁止页面直接请求,便于统一拦截、异常处理、请求优化。
|
||||
|
||||
- 严格遵循命名规范、目录规范,禁止随意修改目录结构、文件名,确保团队开发一致性。
|
||||
|
||||
- 所有代码必须使用TypeScript,禁止any类型,确保类型安全,降低后期维护成本。
|
||||
|
||||
- API服务统一采用实例化调用,禁止静态调用,确保每个服务实例可独立配置,适配多场景需求。
|
||||
|
||||
# 十、架构优势总结
|
||||
|
||||
- 规范化统一:状态、工具、API服务、目录、命名全流程规范,团队开发无歧义,降低协作成本,前期立死规矩,后期开发更快、更不乱。
|
||||
|
||||
- 开发高效:公共模块(Stores、Composables、EXTEND工具、API服务)自动导入,无需重复编写import,降低冗余代码,API服务可直接实例化调用,大幅提升开发效率。
|
||||
|
||||
- 可维护性强:分层清晰,模块职责单一,状态变更可追溯,工具与API服务统一管控,后期迭代、修改只需动一处,降低维护成本。
|
||||
|
||||
- 稳定性高:状态变更有管控,工具层纯逻辑封装,API服务统一请求/响应处理,长连接常驻布局,登录态持久化兜底,避免全局污染、数据混乱,适配高并发场景。
|
||||
|
||||
- 可扩展性强:模块化拆分,插件化扩展友好,IM长连接、多插件商城、用户体系可随意扩展,API服务与工具库可独立新增,架构不崩解,一步到位终身舒服。
|
||||
|
||||
- 类型安全:全程TypeScript支持,类型定义统一收口,API请求/响应、工具方法、状态均有类型约束,避免类型错误,提升代码健壮性,降低线上bug率。
|
||||
|
||||
# 十一、附录:常用命令与依赖清单
|
||||
|
||||
## 11.1 常用命令
|
||||
|
||||
```bash
|
||||
# 初始化Nuxt4项目(src目录模式)
|
||||
npx nuxi init my-project --src-dir
|
||||
|
||||
# 进入项目目录
|
||||
cd my-project
|
||||
|
||||
# 安装核心依赖
|
||||
npm install pinia @pinia/nuxt @pinia-plugin-persistedstate/nuxt
|
||||
|
||||
# 安装其他常用依赖(根据需求)
|
||||
npm install axios ofetch scss
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
|
||||
# 构建生产包
|
||||
npm run build
|
||||
|
||||
# 预览生产包
|
||||
npm run preview
|
||||
```
|
||||
|
||||
## 11.2 核心依赖清单
|
||||
|
||||
|依赖名称|用途|
|
||||
|---|---|
|
||||
|pinia|全局状态管理核心库|
|
||||
|@pinia/nuxt|Pinia适配Nuxt4的模块,支持自动导入|
|
||||
|@pinia-plugin-persistedstate/nuxt|Pinia持久化插件,实现状态本地备份|
|
||||
|ofetch|网络请求工具,统一封装请求逻辑(RequestEXTEND基础)|
|
||||
|axios|备选网络请求工具,可根据需求替换ofetch|
|
||||
|scss|样式预处理器,支持模块化样式|
|
||||
> (注:文档部分内容可能由 AI 生成)
|
||||
@@ -14,7 +14,8 @@ export default defineNuxtConfig({
|
||||
},
|
||||
|
||||
modules: [
|
||||
'@pinia/nuxt'
|
||||
'@pinia/nuxt',
|
||||
'@vant/nuxt'
|
||||
],
|
||||
|
||||
// 自动导入配置 - 使用完整路径
|
||||
|
||||
118
Web/package-lock.json
generated
118
Web/package-lock.json
generated
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "kx-ui-framework",
|
||||
"name": "sea-time",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kx-ui-framework",
|
||||
"name": "sea-time",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@vant/nuxt": "^1.0.7",
|
||||
"pinia": "^3.0.4",
|
||||
"vant": "^4.9.24",
|
||||
"vue": "^3.5.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1128,7 +1130,6 @@
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -1139,7 +1140,6 @@
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@@ -1150,7 +1150,6 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -1177,7 +1176,6 @@
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
@@ -1440,7 +1438,6 @@
|
||||
"version": "3.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.2.tgz",
|
||||
"integrity": "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"c12": "^3.3.3",
|
||||
@@ -3901,7 +3898,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
@@ -3935,6 +3931,60 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vant/nuxt": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@vant/nuxt/-/nuxt-1.0.7.tgz",
|
||||
"integrity": "sha512-YVRJIDVlCCjWBhi0a/YBY0M04XmGwAqCkDSEIDIcbvzNN2z178iqKS23Py+c4hUv570LoKaIZMCQ75IJphJkTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nuxt/kit": "^3.14.159",
|
||||
"magic-string": "^0.29.0",
|
||||
"unplugin": "^1.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vant": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vant/nuxt/node_modules/magic-string": {
|
||||
"version": "0.29.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz",
|
||||
"integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.13"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@vant/nuxt/node_modules/unplugin": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz",
|
||||
"integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.14.0",
|
||||
"webpack-virtual-modules": "^0.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vant/popperjs": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.3.0.tgz",
|
||||
"integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vant/use": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@vant/use/-/use-1.6.0.tgz",
|
||||
"integrity": "sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/nft": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.5.0.tgz",
|
||||
@@ -4259,7 +4309,6 @@
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -4870,7 +4919,6 @@
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
|
||||
"integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^5.0.0",
|
||||
@@ -4899,7 +4947,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
|
||||
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cac": {
|
||||
@@ -4950,7 +4997,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readdirp": "^5.0.0"
|
||||
@@ -5072,14 +5118,12 @@
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
|
||||
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
|
||||
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.10.0"
|
||||
@@ -5517,7 +5561,6 @@
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
|
||||
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/denque": {
|
||||
@@ -5544,7 +5587,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
|
||||
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
@@ -5666,7 +5708,6 @@
|
||||
"version": "17.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.1.tgz",
|
||||
"integrity": "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -5746,7 +5787,6 @@
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz",
|
||||
"integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-module-lexer": {
|
||||
@@ -5902,7 +5942,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
@@ -5980,7 +6019,6 @@
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -6158,7 +6196,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
|
||||
"integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"giget": "dist/cli.mjs"
|
||||
@@ -6387,7 +6424,6 @@
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
@@ -6698,7 +6734,6 @@
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -6751,7 +6786,6 @@
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz",
|
||||
"integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -6761,7 +6795,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.3.0.tgz",
|
||||
"integrity": "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/launch-editor": {
|
||||
@@ -6999,7 +7032,7 @@
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
|
||||
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.4",
|
||||
@@ -7163,7 +7196,6 @@
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
||||
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.16.0",
|
||||
@@ -7176,14 +7208,12 @@
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
|
||||
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mlly/node_modules/pkg-types": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
|
||||
"integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.1.8",
|
||||
@@ -7871,7 +7901,6 @@
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
|
||||
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/on-change": {
|
||||
@@ -8123,7 +8152,6 @@
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/perfect-debounce": {
|
||||
@@ -8142,7 +8170,6 @@
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -8176,7 +8203,6 @@
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
|
||||
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"confbox": "^0.2.2",
|
||||
@@ -8805,7 +8831,6 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
|
||||
"integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defu": "^6.1.6",
|
||||
@@ -8873,7 +8898,6 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 20.19.0"
|
||||
@@ -9119,14 +9143,12 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
|
||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -9804,7 +9826,6 @@
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
@@ -9906,7 +9927,6 @@
|
||||
"version": "1.6.3",
|
||||
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
|
||||
"integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ultrahtml": {
|
||||
@@ -9927,7 +9947,6 @@
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/unctx/-/unctx-2.5.0.tgz",
|
||||
"integrity": "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.15.0",
|
||||
@@ -9940,7 +9959,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0"
|
||||
@@ -9950,7 +9968,6 @@
|
||||
"version": "2.3.11",
|
||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||
"integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
@@ -10227,7 +10244,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untyped/-/untyped-2.0.0.tgz",
|
||||
"integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"citty": "^0.1.6",
|
||||
@@ -10244,7 +10260,6 @@
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
|
||||
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"consola": "^3.2.3"
|
||||
@@ -10310,6 +10325,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vant": {
|
||||
"version": "4.9.24",
|
||||
"resolved": "https://registry.npmjs.org/vant/-/vant-4.9.24.tgz",
|
||||
"integrity": "sha512-tP1A7Vjzv1/B1ljb95Jhv9Q9w6acaaZDJvy6wcKrwGgY0gQZlg+FXLZH/AIKZBE3xvYGDUsv/M7AuGcr/Pqd6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vant/popperjs": "^1.3.0",
|
||||
"@vant/use": "^1.6.0",
|
||||
"@vue/shared": "^3.5.31"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
@@ -10820,7 +10849,6 @@
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vant/nuxt": "^1.0.7",
|
||||
"pinia": "^3.0.4",
|
||||
"vant": "^4.9.24",
|
||||
"vue": "^3.5.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"@nuxt/devtools": "^2.0.0",
|
||||
"@pinia/nuxt": "^0.11.3",
|
||||
"nuxt": "^4.4.2",
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
|
||||
@@ -1,199 +1 @@
|
||||
/*
|
||||
* 全局样式定义
|
||||
* 此文件通过 nuxt.config.ts 全局引用
|
||||
*/
|
||||
|
||||
/* CSS变量定义 - 主题色 */
|
||||
:root {
|
||||
--primary-color: #409eff;
|
||||
--success-color: #67c23a;
|
||||
--warning-color: #e6a23c;
|
||||
--danger-color: #f56c6c;
|
||||
--info-color: #909399;
|
||||
|
||||
--text-color: #303133;
|
||||
--text-color-secondary: #606266;
|
||||
--text-color-placeholder: #c0c4cc;
|
||||
|
||||
--border-color: #dcdfe6;
|
||||
--border-color-light: #e4e7ed;
|
||||
--border-color-lighter: #ebeef5;
|
||||
|
||||
--bg-color: #ffffff;
|
||||
--bg-color-page: #f5f7fa;
|
||||
--bg-color-overlay: #ffffff;
|
||||
|
||||
--border-radius: 4px;
|
||||
--border-radius-small: 2px;
|
||||
|
||||
--box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
--box-shadow-light: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 深色主题 */
|
||||
[data-theme='dark'] {
|
||||
--text-color: #e5eaf3;
|
||||
--text-color-secondary: #a3a6ad;
|
||||
--text-color-placeholder: #8d9095;
|
||||
|
||||
--border-color: #4c4d4f;
|
||||
--border-color-light: #414243;
|
||||
--border-color-lighter: #363637;
|
||||
|
||||
--bg-color: #1d1e1f;
|
||||
--bg-color-page: #141414;
|
||||
--bg-color-overlay: #262727;
|
||||
|
||||
--box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.4);
|
||||
--box-shadow-light: 0 2px 8px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 重置样式 */
|
||||
* {
|
||||
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;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color-page);
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #66b1ff;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
input,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-color-page);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c0c4cc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #909399;
|
||||
}
|
||||
|
||||
/* 工具类 - 文本对齐 */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 工具类 - 间距 */
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.mb-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mb-20 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.p-20 {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 工具类 - 颜色 */
|
||||
.text-primary {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
/* 工具类 - 背景 */
|
||||
.bg-primary {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--warning-color);
|
||||
}
|
||||
|
||||
.bg-danger {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: var(--info-color);
|
||||
}
|
||||
/* 全局扩展样式留空,默认使用 Vant 样式体系。 */
|
||||
|
||||
@@ -1,469 +1,596 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="page-login">
|
||||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1 class="login-title">登录</h1>
|
||||
<p class="login-subtitle">欢迎使用 Kx UI Framework</p>
|
||||
<div class="login-orb login-orb-a"></div>
|
||||
<div class="login-orb login-orb-b"></div>
|
||||
|
||||
<div class="login-shell">
|
||||
<section class="login-hero">
|
||||
<p class="hero-kicker">SEA TIME</p>
|
||||
<h1>欢迎回到航海时代</h1>
|
||||
<p class="hero-copy">
|
||||
这是一套移动端登录页示例,保留 Vant 的按钮与标签组件,表单主体改为 SSR 稳定写法。
|
||||
</p>
|
||||
|
||||
<div class="hero-pills">
|
||||
<span>移动端优先</span>
|
||||
<span>SSR 安全</span>
|
||||
<span>模拟登录</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="login-card">
|
||||
<div class="card-top">
|
||||
<div>
|
||||
<p class="card-eyebrow">账号登录</p>
|
||||
<h2>继续你的航程</h2>
|
||||
</div>
|
||||
<van-tag round plain type="primary">Sample</van-tag>
|
||||
</div>
|
||||
|
||||
<form class="login-form" @submit.prevent="handleLogin">
|
||||
<!-- 用户名 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="username">用户名</label>
|
||||
<div class="card-banner">
|
||||
<span class="banner-dot"></span>
|
||||
默认可直接使用演示账号,也可以手动输入任意符合规则的内容。
|
||||
</div>
|
||||
|
||||
<div class="demo-account">
|
||||
<div>
|
||||
<strong>演示账号</strong>
|
||||
<span>captain</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>演示密码</strong>
|
||||
<span>Voyage123</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="login-form" novalidate @submit.prevent="handleSubmit">
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="login-username">账号</label>
|
||||
<div class="field-box" :class="{ 'is-error': !!errors.username }">
|
||||
<span class="field-marker">A</span>
|
||||
<input
|
||||
id="username"
|
||||
v-model="loginForm.username"
|
||||
id="login-username"
|
||||
v-model.trim="form.username"
|
||||
class="field-input"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="请输入用户名"
|
||||
@blur="validateField('username')"
|
||||
inputmode="text"
|
||||
autocomplete="username"
|
||||
placeholder="请输入账号"
|
||||
@input="clearFieldError('username')"
|
||||
/>
|
||||
<span class="form-error" v-if="errors.username">{{ errors.username }}</span>
|
||||
</div>
|
||||
<p v-if="errors.username" class="field-error">{{ errors.username }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 密码 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="password">密码</label>
|
||||
<div class="field-group">
|
||||
<label class="field-label" for="login-password">密码</label>
|
||||
<div class="field-box" :class="{ 'is-error': !!errors.password }">
|
||||
<span class="field-marker">P</span>
|
||||
<input
|
||||
id="password"
|
||||
v-model="loginForm.password"
|
||||
id="login-password"
|
||||
v-model.trim="form.password"
|
||||
class="field-input"
|
||||
type="password"
|
||||
class="form-input"
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
@blur="validateField('password')"
|
||||
@input="clearFieldError('password')"
|
||||
/>
|
||||
<span class="form-error" v-if="errors.password">{{ errors.password }}</span>
|
||||
</div>
|
||||
<p v-if="errors.password" class="field-error">{{ errors.password }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<div class="form-item">
|
||||
<label class="form-label" for="captcha">验证码</label>
|
||||
<div class="captcha-wrapper">
|
||||
<input
|
||||
id="captcha"
|
||||
v-model="loginForm.captcha"
|
||||
type="text"
|
||||
class="form-input captcha-input"
|
||||
placeholder="请输入验证码"
|
||||
@blur="validateField('captcha')"
|
||||
/>
|
||||
<div class="captcha-code" @click="refreshCaptcha">
|
||||
{{ captchaCode }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="form-error" v-if="errors.captcha">{{ errors.captcha }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 记住密码 & 忘记密码 -->
|
||||
<div class="form-options">
|
||||
<label class="checkbox-label">
|
||||
<input v-model="loginForm.remember" type="checkbox" />
|
||||
<span>记住密码</span>
|
||||
<div class="login-options">
|
||||
<label class="check-row">
|
||||
<input v-model="form.remember" class="check-input" type="checkbox" />
|
||||
<span class="check-box"></span>
|
||||
<span class="check-text">记住演示账号</span>
|
||||
</label>
|
||||
<a href="javascript:;" class="forgot-link">忘记密码?</a>
|
||||
|
||||
<button class="ghost-link" type="button" @click="fillDemoAccount">一键填充</button>
|
||||
<van-button type="primary" to="/home" size="mini" :loading="false">路由跳转</van-button>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button type="submit" class="login-btn" :disabled="isLoading">
|
||||
{{ isLoading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
<div class="login-agreement">
|
||||
<label class="check-row">
|
||||
<input
|
||||
v-model="form.agreement"
|
||||
class="check-input"
|
||||
type="checkbox"
|
||||
@change="clearFieldError('agreement')"
|
||||
/>
|
||||
<span class="check-box"></span>
|
||||
<span class="check-text">我已阅读并同意演示使用说明</span>
|
||||
</label>
|
||||
<p v-if="errors.agreement" class="field-error agreement-error">{{ errors.agreement }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 注册链接 -->
|
||||
<div class="login-footer">
|
||||
<span class="register-tip">还没有账号?</span>
|
||||
<a href="javascript:;" class="register-link">立即注册</a>
|
||||
<div class="login-actions">
|
||||
<van-button
|
||||
block
|
||||
round
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="isSubmitting"
|
||||
:disabled="submitDisabled"
|
||||
>
|
||||
模拟登录
|
||||
</van-button>
|
||||
<van-button block round plain type="primary" native-type="button" @click="clearForm">
|
||||
清空输入
|
||||
</van-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="login-tips" v-if="tips.show">
|
||||
<p :class="['tip-text', tips.type]">{{ tips.message }}</p>
|
||||
<div class="login-tips">
|
||||
<div class="tip-item">
|
||||
<span>01</span>
|
||||
去掉 ClientOnly,刷新时不再有表单占位替换。
|
||||
</div>
|
||||
<div class="tip-item">
|
||||
<span>02</span>
|
||||
提交后延迟模拟请求,并跳转到主页。
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// stores、extends、services 目录下的内容已自动导入
|
||||
// types目录需要手动import type
|
||||
|
||||
import type { ILoginParams } from '~/types/api'
|
||||
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: layout.empty
|
||||
})
|
||||
|
||||
// Pinia状态仓库(自动导入)
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
type ErrorField = 'username' | 'password' | 'agreement'
|
||||
|
||||
// 登录表单数据
|
||||
const loginForm = reactive({
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: '',
|
||||
remember: false
|
||||
remember: true,
|
||||
agreement: true
|
||||
})
|
||||
|
||||
// 表单校验错误
|
||||
const errors = reactive({
|
||||
const errors = reactive<Record<ErrorField, string>>({
|
||||
username: '',
|
||||
password: '',
|
||||
captcha: ''
|
||||
agreement: ''
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const isLoading = ref(false)
|
||||
|
||||
// 验证码
|
||||
const captchaCode = ref('')
|
||||
|
||||
// 提示信息
|
||||
const tips = reactive({
|
||||
show: false,
|
||||
message: '',
|
||||
type: 'info'
|
||||
const submitDisabled = computed(() => {
|
||||
return !form.username.trim() || !form.password.trim() || !form.agreement
|
||||
})
|
||||
|
||||
// 生成随机验证码
|
||||
const generateCaptcha = () => {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
let code = ''
|
||||
for (let i = 0; i < 4; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
}
|
||||
captchaCode.value = code
|
||||
}
|
||||
|
||||
// 刷新验证码
|
||||
const refreshCaptcha = () => {
|
||||
generateCaptcha()
|
||||
}
|
||||
|
||||
// 校验单个字段
|
||||
const validateField = (field: keyof typeof errors) => {
|
||||
const clearFieldError = (field: ErrorField) => {
|
||||
errors[field] = ''
|
||||
|
||||
switch (field) {
|
||||
case 'username':
|
||||
if (!loginForm.username) {
|
||||
errors.username = '请输入用户名'
|
||||
} else if (!ValidateEXTEND.minLength(loginForm.username, 3)) {
|
||||
errors.username = '用户名至少3个字符'
|
||||
}
|
||||
break
|
||||
case 'password':
|
||||
if (!loginForm.password) {
|
||||
errors.password = '请输入密码'
|
||||
} else if (!ValidateEXTEND.minLength(loginForm.password, 6)) {
|
||||
errors.password = '密码至少6个字符'
|
||||
}
|
||||
break
|
||||
case 'captcha':
|
||||
if (!loginForm.captcha) {
|
||||
errors.captcha = '请输入验证码'
|
||||
} else if (loginForm.captcha.toUpperCase() !== captchaCode.value.toUpperCase()) {
|
||||
errors.captcha = '验证码错误'
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return !errors[field]
|
||||
}
|
||||
|
||||
// 校验整个表单
|
||||
const validateForm = () => {
|
||||
const usernameValid = validateField('username')
|
||||
const passwordValid = validateField('password')
|
||||
const captchaValid = validateField('captcha')
|
||||
return usernameValid && passwordValid && captchaValid
|
||||
errors.username = ''
|
||||
errors.password = ''
|
||||
errors.agreement = ''
|
||||
|
||||
if (!form.username.trim()) {
|
||||
errors.username = '请输入账号'
|
||||
} else if (form.username.trim().length < 3) {
|
||||
errors.username = '账号至少 3 位'
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
const showTips = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||
tips.message = message
|
||||
tips.type = type
|
||||
tips.show = true
|
||||
|
||||
setTimeout(() => {
|
||||
tips.show = false
|
||||
}, 3000)
|
||||
if (!form.password.trim()) {
|
||||
errors.password = '请输入密码'
|
||||
} else if (form.password.trim().length < 6) {
|
||||
errors.password = '密码至少 6 位'
|
||||
}
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
// 校验表单
|
||||
if (!form.agreement) {
|
||||
errors.agreement = '请先勾选使用说明'
|
||||
}
|
||||
|
||||
return !errors.username && !errors.password && !errors.agreement
|
||||
}
|
||||
|
||||
const fillDemoAccount = () => {
|
||||
form.username = 'captain'
|
||||
form.password = 'Voyage123'
|
||||
errors.username = ''
|
||||
errors.password = ''
|
||||
showToast('已填充演示账号')
|
||||
}
|
||||
|
||||
const clearForm = () => {
|
||||
form.username = ''
|
||||
form.password = ''
|
||||
form.agreement = true
|
||||
errors.username = ''
|
||||
errors.password = ''
|
||||
errors.agreement = ''
|
||||
showToast('已清空输入')
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) {
|
||||
showFailToast(Object.values(errors).find(Boolean) || '请检查表单内容')
|
||||
return
|
||||
}
|
||||
|
||||
// 开始加载
|
||||
isLoading.value = true
|
||||
userStore.setLoginLoading(true)
|
||||
|
||||
try {
|
||||
// 模拟登录(实际项目中调用API)
|
||||
// const userService = new UserSERVICE()
|
||||
// const result = await userService.login(loginForm as ILoginParams)
|
||||
|
||||
// 模拟登录成功
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
// 设置用户信息
|
||||
userStore.setUserInfo(
|
||||
{
|
||||
id: 1001,
|
||||
username: loginForm.username,
|
||||
nickname: loginForm.username,
|
||||
role: 'admin'
|
||||
},
|
||||
'demo-token-' + Date.now()
|
||||
)
|
||||
|
||||
showTips('登录成功!', 'success')
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
router.push('/home')
|
||||
}, 1000)
|
||||
} catch (error: any) {
|
||||
console.error('登录失败:', error)
|
||||
showTips(error.message || '登录失败,请稍后重试', 'error')
|
||||
// 刷新验证码
|
||||
refreshCaptcha()
|
||||
loginForm.captcha = ''
|
||||
isSubmitting.value = true
|
||||
await new Promise((resolve) => setTimeout(resolve, 900))
|
||||
showSuccessToast('登录成功')
|
||||
await navigateTo('/home')
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
userStore.setLoginLoading(false)
|
||||
isSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 如果已登录,跳转到首页
|
||||
if (userStore.isLogin) {
|
||||
router.push('/home')
|
||||
return
|
||||
}
|
||||
|
||||
// 生成验证码
|
||||
generateCaptcha()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-login {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
padding: 28px 16px 40px;
|
||||
background:
|
||||
linear-gradient(180deg, #0b3558 0%, #0f2943 46%, #f4f8fc 46%, #f4f8fc 100%);
|
||||
}
|
||||
|
||||
.login-orb {
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
filter: blur(4px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.login-orb-a {
|
||||
top: -36px;
|
||||
right: -22px;
|
||||
width: 148px;
|
||||
height: 148px;
|
||||
background: radial-gradient(circle, rgba(103, 232, 249, 0.56), rgba(103, 232, 249, 0));
|
||||
}
|
||||
|
||||
.login-orb-b {
|
||||
top: 168px;
|
||||
left: -52px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: radial-gradient(circle, rgba(96, 165, 250, 0.24), rgba(96, 165, 250, 0));
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.login-hero {
|
||||
padding: 8px 6px 24px;
|
||||
color: #f8fbff;
|
||||
}
|
||||
|
||||
.hero-kicker {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.28em;
|
||||
color: rgba(186, 230, 253, 0.92);
|
||||
}
|
||||
|
||||
.login-hero h1 {
|
||||
margin: 0;
|
||||
font-size: 34px;
|
||||
line-height: 1.08;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
margin: 14px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: rgba(226, 232, 240, 0.92);
|
||||
}
|
||||
|
||||
.hero-pills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.hero-pills span {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid rgba(186, 230, 253, 0.18);
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
color: #dbeafe;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin-top: 8px;
|
||||
padding: 22px 16px 18px;
|
||||
border-radius: 28px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 28px 60px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.card-eyebrow {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.card-top h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.card-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #0f3d62;
|
||||
background: linear-gradient(135deg, #e0f2fe, #eff6ff);
|
||||
}
|
||||
|
||||
.login-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 20px;
|
||||
.banner-dot {
|
||||
flex: 0 0 auto;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0ea5e9, #2563eb);
|
||||
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.12);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
.demo-account {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
.demo-account div {
|
||||
padding: 12px 14px;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 10px;
|
||||
.demo-account strong,
|
||||
.demo-account span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
.demo-account strong {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.demo-account span {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
.field-group + .field-group {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
.field-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
color: var(--text-color);
|
||||
background-color: var(--bg-color-page);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-color-placeholder);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--danger-color);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.captcha-wrapper {
|
||||
.field-box {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 52px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.captcha-input {
|
||||
flex: 1;
|
||||
.field-box:focus-within {
|
||||
border-color: #3b82f6;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.captcha-code {
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
.field-box.is-error {
|
||||
border-color: #f87171;
|
||||
}
|
||||
|
||||
.field-marker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--bg-color-page);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 4px;
|
||||
color: var(--primary-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #2563eb;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.captcha-code:hover {
|
||||
background-color: var(--border-color-lighter);
|
||||
}
|
||||
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.forgot-link {
|
||||
font-size: 14px;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
.field-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.login-btn:hover:not(:disabled) {
|
||||
background-color: #66b1ff;
|
||||
.field-input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
background-color: var(--border-color);
|
||||
cursor: not-allowed;
|
||||
.field-input:-webkit-autofill,
|
||||
.field-input:-webkit-autofill:hover,
|
||||
.field-input:-webkit-autofill:focus {
|
||||
-webkit-text-fill-color: #0f172a;
|
||||
box-shadow: 0 0 0 1000px #f8fafc inset;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
.field-error {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.register-tip {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
.login-options,
|
||||
.login-agreement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 2px 0;
|
||||
}
|
||||
|
||||
.register-link {
|
||||
font-size: 14px;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
margin-left: 5px;
|
||||
.login-agreement {
|
||||
display: block;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.register-link:hover {
|
||||
text-decoration: underline;
|
||||
.agreement-error {
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.check-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.check-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.check-box {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1.5px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.check-box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 5px;
|
||||
width: 4px;
|
||||
height: 9px;
|
||||
border-right: 2px solid #ffffff;
|
||||
border-bottom: 2px solid #ffffff;
|
||||
opacity: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.check-input:checked + .check-box {
|
||||
border-color: #2563eb;
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.check-input:checked + .check-box::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.check-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ghost-link {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: #2563eb;
|
||||
background: transparent;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.login-tips {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.tip-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #334155;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.tip-item span {
|
||||
flex: 0 0 auto;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
color: #2563eb;
|
||||
background: #dbeafe;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
@media (min-width: 768px) {
|
||||
.page-login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 36px 20px;
|
||||
}
|
||||
|
||||
.tip-text.success {
|
||||
color: var(--success-color);
|
||||
background-color: rgba(103, 194, 58, 0.1);
|
||||
.login-shell {
|
||||
max-width: 460px;
|
||||
}
|
||||
|
||||
.tip-text.error {
|
||||
color: var(--danger-color);
|
||||
background-color: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
.tip-text.info {
|
||||
color: var(--info-color);
|
||||
background-color: rgba(144, 147, 153, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -1,438 +1,15 @@
|
||||
<template>
|
||||
<div class="page-home">
|
||||
<div class="container">
|
||||
<h1 class="page-title">Nuxt4 + Pinia 前端框架 Demo</h1>
|
||||
<p class="page-desc">本页面展示了框架各核心模块的调用方式</p>
|
||||
|
||||
<!-- 1. Pinia状态管理Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">1. Pinia 状态管理</h2>
|
||||
<div class="demo-card">
|
||||
<div class="demo-item">
|
||||
<span class="label">登录状态:</span>
|
||||
<span class="value">{{ userStore.isLogin ? '已登录' : '未登录' }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">用户ID:</span>
|
||||
<span class="value">{{ userStore.userId || '-' }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">用户昵称:</span>
|
||||
<span class="value">{{ userStore.userNickname }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">用户角色:</span>
|
||||
<span class="value">{{ userStore.userRole }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="setUserInfo">设置用户信息</button>
|
||||
<button class="btn btn-danger" @click="clearUserInfo">清除用户信息</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. EXTEND工具库Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">2. EXTEND 工具库</h2>
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">DateEXTEND - 日期工具</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">当前时间戳:</span>
|
||||
<span class="value">{{ currentTimestamp }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">格式化时间:</span>
|
||||
<span class="value">{{ formattedTime }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">智能时间显示:</span>
|
||||
<span class="value">{{ smartTime }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">是否今天:</span>
|
||||
<span class="value">{{ isToday ? '是' : '否' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">CryptoEXTEND - 加密工具</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">原始字符串:</span>
|
||||
<span class="value">{{ originalText }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">Base64加密:</span>
|
||||
<span class="value">{{ encryptedBase64 }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">自定义密钥加密:</span>
|
||||
<span class="value">{{ encryptedWithKey }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">自定义密钥解密:</span>
|
||||
<span class="value">{{ decryptedWithKey }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testCrypto">测试加密解密</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">ValidateEXTEND - 校验工具</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">手机号校验:</span>
|
||||
<span class="value">{{ validateResult.phone }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">邮箱校验:</span>
|
||||
<span class="value">{{ validateResult.email }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">身份证校验:</span>
|
||||
<span class="value">{{ validateResult.idCard }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">密码强度校验:</span>
|
||||
<span class="value">{{ validateResult.password }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testValidate">测试校验</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. API服务Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">3. API 服务调用</h2>
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">UserSERVICE - 用户服务</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">服务调用方式:</span>
|
||||
<span class="value">const userService = new UserSERVICE()</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">可用方法:</span>
|
||||
<span class="value">login(), getInfo(), logout(), updateInfo(), changePassword()</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testUserService">测试用户服务</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">CommonSERVICE - 通用服务</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">服务调用方式:</span>
|
||||
<span class="value">const commonService = new CommonSERVICE()</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">可用方法:</span>
|
||||
<span class="value">getDict(), upload(), getConfig(), sendSmsCode()</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testCommonService">测试通用服务</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. Composables Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">4. Composables 组合式函数</h2>
|
||||
<div class="demo-card">
|
||||
<h3 class="card-title">useAuth - 权限校验</h3>
|
||||
<div class="demo-item">
|
||||
<span class="label">是否已登录:</span>
|
||||
<span class="value">{{ authState.isLogin ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">是否为管理员:</span>
|
||||
<span class="value">{{ authState.isAdmin ? '是' : '否' }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="testAuth">测试权限校验</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 5. App状态Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">5. 应用状态管理</h2>
|
||||
<div class="demo-card">
|
||||
<div class="demo-item">
|
||||
<span class="label">当前主题:</span>
|
||||
<span class="value">{{ appStore.theme }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">当前语言:</span>
|
||||
<span class="value">{{ appStore.locale }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">设备类型:</span>
|
||||
<span class="value">{{ appStore.device }}</span>
|
||||
</div>
|
||||
<div class="demo-item">
|
||||
<span class="label">屏幕尺寸:</span>
|
||||
<span class="value">{{ appStore.screenWidth }} x {{ appStore.screenHeight }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<button class="btn btn-primary" @click="toggleTheme">切换主题</button>
|
||||
<button class="btn" @click="setLoading">测试加载态</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 6. 路由Demo -->
|
||||
<section class="demo-section">
|
||||
<h2 class="section-title">6. 路由导航</h2>
|
||||
<div class="demo-card">
|
||||
<div class="demo-item">
|
||||
<span class="label">当前路径:</span>
|
||||
<span class="value">{{ route.path }}</span>
|
||||
</div>
|
||||
<div class="demo-actions">
|
||||
<NuxtLink to="/auth/login" class="btn btn-primary">跳转到登录页</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<template>
|
||||
<div class="page-home"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// stores、composables、extends、services 目录下的内容已自动导入
|
||||
// 无需手动import,直接使用即可
|
||||
|
||||
// 使用Pinia状态仓库(自动导入)
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 使用Composables
|
||||
const auth = useAuth()
|
||||
|
||||
// 路由
|
||||
const route = useRoute()
|
||||
|
||||
// DateEXTEND Demo
|
||||
// 使用客户端时间,避免SSR水合问题
|
||||
const currentTimestamp = ref(0)
|
||||
const formattedTime = ref('')
|
||||
const smartTime = ref('')
|
||||
const isToday = ref(false)
|
||||
|
||||
// 在客户端初始化时间
|
||||
if (import.meta.client) {
|
||||
currentTimestamp.value = DateEXTEND.now()
|
||||
formattedTime.value = DateEXTEND.format(Date.now())
|
||||
smartTime.value = DateEXTEND.smartFormat(Date.now() - 3600000)
|
||||
isToday.value = DateEXTEND.isToday(Date.now())
|
||||
}
|
||||
|
||||
// CryptoEXTEND Demo
|
||||
const originalText = ref('Hello Kx Framework')
|
||||
const encryptedBase64 = ref('')
|
||||
const encryptedWithKey = ref('')
|
||||
const decryptedWithKey = ref('')
|
||||
|
||||
// ValidateEXTEND Demo
|
||||
const validateResult = ref({
|
||||
phone: '',
|
||||
email: '',
|
||||
idCard: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
// Auth Demo
|
||||
const authState = computed(() => ({
|
||||
isLogin: auth.isLogin.value,
|
||||
isAdmin: auth.isAdmin()
|
||||
}))
|
||||
|
||||
// 测试设置用户信息
|
||||
const setUserInfo = () => {
|
||||
userStore.setUserInfo(
|
||||
{
|
||||
id: 1001,
|
||||
username: 'admin',
|
||||
nickname: '管理员',
|
||||
role: 'admin'
|
||||
},
|
||||
'demo-token-12345'
|
||||
)
|
||||
}
|
||||
|
||||
// 测试清除用户信息
|
||||
const clearUserInfo = () => {
|
||||
userStore.clearUserInfo()
|
||||
}
|
||||
|
||||
// 测试加密解密
|
||||
const testCrypto = () => {
|
||||
// Base64加密
|
||||
const crypto = new CryptoEXTEND()
|
||||
encryptedBase64.value = crypto.encryptBase64(originalText.value)
|
||||
|
||||
// 自定义密钥加密
|
||||
const customCrypto = new CryptoEXTEND('my-secret-key')
|
||||
encryptedWithKey.value = customCrypto.encrypt(originalText.value)
|
||||
decryptedWithKey.value = customCrypto.decrypt(encryptedWithKey.value)
|
||||
}
|
||||
|
||||
// 测试校验工具
|
||||
const testValidate = () => {
|
||||
validateResult.value = {
|
||||
phone: ValidateEXTEND.isPhone('13800138000') ? '有效' : '无效',
|
||||
email: ValidateEXTEND.isEmail('test@example.com') ? '有效' : '无效',
|
||||
idCard: ValidateEXTEND.isIdCard('110101199001011234') ? '有效' : '无效',
|
||||
password: ValidateEXTEND.isPassword('Aa123456') ? '有效' : '无效'
|
||||
}
|
||||
}
|
||||
|
||||
// 测试用户服务
|
||||
const testUserService = () => {
|
||||
const userService = new UserSERVICE()
|
||||
console.log('UserSERVICE 实例创建成功:', userService)
|
||||
console.log('可用方法: login(), getInfo(), logout(), updateInfo(), changePassword()')
|
||||
alert('UserSERVICE 实例已创建,请查看控制台输出')
|
||||
}
|
||||
|
||||
// 测试通用服务
|
||||
const testCommonService = () => {
|
||||
const commonService = new CommonSERVICE()
|
||||
console.log('CommonSERVICE 实例创建成功:', commonService)
|
||||
console.log('可用方法: getDict(), upload(), getConfig(), sendSmsCode()')
|
||||
alert('CommonSERVICE 实例已创建,请查看控制台输出')
|
||||
}
|
||||
|
||||
// 测试权限校验
|
||||
const testAuth = () => {
|
||||
console.log('isLogin:', auth.isLogin.value)
|
||||
console.log('userId:', auth.getUserId())
|
||||
console.log('nickname:', auth.getNickname())
|
||||
console.log('hasRole(admin):', auth.hasRole('admin'))
|
||||
console.log('isAdmin:', auth.isAdmin())
|
||||
alert('权限校验测试完成,请查看控制台输出')
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
appStore.toggleTheme()
|
||||
}
|
||||
|
||||
// 测试加载态
|
||||
const setLoading = () => {
|
||||
appStore.startLoading('正在加载数据...')
|
||||
setTimeout(() => {
|
||||
appStore.endLoading()
|
||||
alert('加载完成')
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 初始化测试
|
||||
onMounted(() => {
|
||||
testCrypto()
|
||||
testValidate()
|
||||
definePageMeta({
|
||||
layout: layout.default
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-home {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 15px;
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: var(--box-shadow-light);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color-lighter);
|
||||
}
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.demo-item .label {
|
||||
width: 160px;
|
||||
color: var(--text-color-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.demo-item .value {
|
||||
color: var(--text-color);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
background-color: var(--bg-color-page);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger-color);
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -5,9 +5,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
router.replace('/home')
|
||||
})
|
||||
await navigateTo('/auth/login', { replace: true })
|
||||
</script>
|
||||
@@ -1,148 +1,69 @@
|
||||
/**
|
||||
* 应用状态仓库
|
||||
* 管理全局应用状态,如主题、加载态、配置等
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
// 主题类型
|
||||
export type ThemeMode = 'light' | 'dark' | 'auto'
|
||||
|
||||
// 语言类型
|
||||
export type Locale = 'zh-CN' | 'en-US'
|
||||
|
||||
// 应用配置
|
||||
export interface IAppConfig {
|
||||
theme: ThemeMode
|
||||
locale: Locale
|
||||
sidebarCollapsed: boolean
|
||||
showDebug: boolean
|
||||
}
|
||||
|
||||
// localStorage key
|
||||
const STORAGE_KEY = 'app-config-store'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
// 1. 原始状态
|
||||
state: () => ({
|
||||
// 主题模式
|
||||
theme: 'light' as ThemeMode,
|
||||
// 当前语言
|
||||
locale: 'zh-CN' as Locale,
|
||||
// 侧边栏是否收起
|
||||
sidebarCollapsed: false,
|
||||
// 是否显示调试信息
|
||||
showDebug: false,
|
||||
// 全局加载态
|
||||
isLoading: false,
|
||||
// 加载提示文字
|
||||
loadingText: '',
|
||||
// 设备类型
|
||||
device: 'desktop' as 'desktop' | 'mobile' | 'tablet',
|
||||
// 浏览器是否在线
|
||||
isOnline: true,
|
||||
// 屏幕宽度
|
||||
screenWidth: 0,
|
||||
// 屏幕高度
|
||||
screenHeight: 0
|
||||
}),
|
||||
|
||||
// 2. 只读计算属性
|
||||
getters: {
|
||||
// 判断是否为浅色主题
|
||||
isLightTheme: (state) => state.theme === 'light',
|
||||
|
||||
// 判断是否为深色主题
|
||||
isDarkTheme: (state) => state.theme === 'dark',
|
||||
|
||||
// 判断是否为移动端
|
||||
isMobile: (state) => state.device === 'mobile',
|
||||
|
||||
// 判断是否为平板
|
||||
isTablet: (state) => state.device === 'tablet',
|
||||
|
||||
// 判断是否为桌面端
|
||||
isDesktop: (state) => state.device === 'desktop',
|
||||
|
||||
// 获取主题class
|
||||
themeClass: (state) => `theme-${state.theme}`,
|
||||
|
||||
// 判断是否有加载态
|
||||
hasLoading: (state) => state.isLoading
|
||||
},
|
||||
|
||||
// 3. 状态修改入口
|
||||
actions: {
|
||||
// 设置主题
|
||||
setTheme(theme: ThemeMode) {
|
||||
this.theme = theme
|
||||
this.syncToLocalStorage()
|
||||
// 应用主题到html元素
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
}
|
||||
},
|
||||
|
||||
// 切换主题
|
||||
toggleTheme() {
|
||||
this.theme = this.theme === 'light' ? 'dark' : 'light'
|
||||
this.syncToLocalStorage()
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', this.theme)
|
||||
}
|
||||
},
|
||||
|
||||
// 设置语言
|
||||
setLocale(locale: Locale) {
|
||||
this.locale = locale
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 切换侧边栏收起状态
|
||||
toggleSidebar() {
|
||||
this.sidebarCollapsed = !this.sidebarCollapsed
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 设置侧边栏收起状态
|
||||
setSidebarCollapsed(collapsed: boolean) {
|
||||
this.sidebarCollapsed = collapsed
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 设置调试模式
|
||||
setShowDebug(show: boolean) {
|
||||
this.showDebug = show
|
||||
this.syncToLocalStorage()
|
||||
},
|
||||
|
||||
// 开始加载
|
||||
startLoading(text: string = '加载中...') {
|
||||
this.isLoading = true
|
||||
this.loadingText = text
|
||||
},
|
||||
|
||||
// 结束加载
|
||||
endLoading() {
|
||||
this.isLoading = false
|
||||
this.loadingText = ''
|
||||
},
|
||||
|
||||
// 设置设备类型
|
||||
setDevice(device: 'desktop' | 'mobile' | 'tablet') {
|
||||
this.device = device
|
||||
},
|
||||
|
||||
// 设置网络状态
|
||||
setOnlineStatus(isOnline: boolean) {
|
||||
this.isOnline = isOnline
|
||||
},
|
||||
|
||||
// 更新屏幕尺寸
|
||||
updateScreenSize(width: number, height: number) {
|
||||
this.screenWidth = width
|
||||
this.screenHeight = height
|
||||
|
||||
// 根据宽度自动判断设备类型
|
||||
if (width < 768) {
|
||||
this.device = 'mobile'
|
||||
} else if (width < 1024) {
|
||||
@@ -152,18 +73,12 @@ export const useAppStore = defineStore('app', {
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化应用配置
|
||||
initConfig(config: Partial<IAppConfig>) {
|
||||
if (config.theme) this.setTheme(config.theme)
|
||||
if (config.locale) this.setLocale(config.locale)
|
||||
if (config.sidebarCollapsed !== undefined) this.setSidebarCollapsed(config.sidebarCollapsed)
|
||||
if (config.showDebug !== undefined) this.setShowDebug(config.showDebug)
|
||||
},
|
||||
|
||||
// 重置所有状态
|
||||
reset() {
|
||||
this.theme = 'light'
|
||||
this.locale = 'zh-CN'
|
||||
this.sidebarCollapsed = false
|
||||
this.showDebug = false
|
||||
this.isLoading = false
|
||||
@@ -171,12 +86,9 @@ export const useAppStore = defineStore('app', {
|
||||
this.clearLocalStorage()
|
||||
},
|
||||
|
||||
// 同步到localStorage
|
||||
syncToLocalStorage() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const data = {
|
||||
theme: this.theme,
|
||||
locale: this.locale,
|
||||
sidebarCollapsed: this.sidebarCollapsed,
|
||||
showDebug: this.showDebug
|
||||
}
|
||||
@@ -184,21 +96,14 @@ export const useAppStore = defineStore('app', {
|
||||
}
|
||||
},
|
||||
|
||||
// 从localStorage恢复
|
||||
restoreFromLocalStorage() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
try {
|
||||
const data = JSON.parse(stored)
|
||||
this.theme = data.theme || 'light'
|
||||
this.locale = data.locale || 'zh-CN'
|
||||
this.sidebarCollapsed = data.sidebarCollapsed || false
|
||||
this.showDebug = data.showDebug || false
|
||||
// 应用主题
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', this.theme)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('恢复应用配置失败:', e)
|
||||
}
|
||||
@@ -206,7 +111,6 @@ export const useAppStore = defineStore('app', {
|
||||
}
|
||||
},
|
||||
|
||||
// 清除localStorage
|
||||
clearLocalStorage() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
|
||||
Reference in New Issue
Block a user