This commit is contained in:
Putoo
2026-04-21 20:36:13 +08:00
parent 6a2028e366
commit 996bdbbd1c
9 changed files with 1243 additions and 1134 deletions

672
Web/docs/remand.md Normal file
View 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/EXTENDAPI服务调用后端接口获取数据EXTEND工具处理数据逻辑返回处理后的数据。
4. StatePinia仓库通过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 生成)

View File

@@ -14,7 +14,8 @@ export default defineNuxtConfig({
},
modules: [
'@pinia/nuxt'
'@pinia/nuxt',
'@vant/nuxt'
],
// 自动导入配置 - 使用完整路径

118
Web/package-lock.json generated
View File

@@ -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": {

View File

@@ -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"
}

View File

@@ -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 样式体系。 */

View File

@@ -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>
<input
id="username"
v-model="loginForm.username"
type="text"
class="form-input"
placeholder="请输入用户名"
@blur="validateField('username')"
/>
<span class="form-error" v-if="errors.username">{{ errors.username }}</span>
</div>
<div class="card-banner">
<span class="banner-dot"></span>
默认可直接使用演示账号也可以手动输入任意符合规则的内容
</div>
<!-- 密码 -->
<div class="form-item">
<label class="form-label" for="password">密码</label>
<input
id="password"
v-model="loginForm.password"
type="password"
class="form-input"
placeholder="请输入密码"
@blur="validateField('password')"
/>
<span class="form-error" v-if="errors.password">{{ errors.password }}</span>
<div class="demo-account">
<div>
<strong>演示账号</strong>
<span>captain</span>
</div>
<div>
<strong>演示密码</strong>
<span>Voyage123</span>
</div>
</div>
<!-- 验证码 -->
<div class="form-item">
<label class="form-label" for="captcha">验证码</label>
<div class="captcha-wrapper">
<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="captcha"
v-model="loginForm.captcha"
id="login-username"
v-model.trim="form.username"
class="field-input"
type="text"
class="form-input captcha-input"
placeholder="请输入验证码"
@blur="validateField('captcha')"
inputmode="text"
autocomplete="username"
placeholder="请输入账号"
@input="clearFieldError('username')"
/>
<div class="captcha-code" @click="refreshCaptcha">
{{ captchaCode }}
</div>
</div>
<span class="form-error" v-if="errors.captcha">{{ errors.captcha }}</span>
<p v-if="errors.username" class="field-error">{{ errors.username }}</p>
</div>
<!-- 记住密码 & 忘记密码 -->
<div class="form-options">
<label class="checkbox-label">
<input v-model="loginForm.remember" type="checkbox" />
<span>记住密码</span>
<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="login-password"
v-model.trim="form.password"
class="field-input"
type="password"
autocomplete="current-password"
placeholder="请输入密码"
@input="clearFieldError('password')"
/>
</div>
<p v-if="errors.password" class="field-error">{{ errors.password }}</p>
</div>
<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>
</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
const validateForm = () => {
errors.username = ''
errors.password = ''
errors.agreement = ''
if (!form.username.trim()) {
errors.username = '请输入账号'
} else if (form.username.trim().length < 3) {
errors.username = '账号至少 3 位'
}
return !errors[field]
if (!form.password.trim()) {
errors.password = '请输入密码'
} else if (form.password.trim().length < 6) {
errors.password = '密码至少 6 位'
}
if (!form.agreement) {
errors.agreement = '请先勾选使用说明'
}
return !errors.username && !errors.password && !errors.agreement
}
// 校验整个表单
const validateForm = () => {
const usernameValid = validateField('username')
const passwordValid = validateField('password')
const captchaValid = validateField('captcha')
return usernameValid && passwordValid && captchaValid
const fillDemoAccount = () => {
form.username = 'captain'
form.password = 'Voyage123'
errors.username = ''
errors.password = ''
showToast('已填充演示账号')
}
// 显示提示
const showTips = (message: string, type: 'success' | 'error' | 'info' = 'info') => {
tips.message = message
tips.type = type
tips.show = true
setTimeout(() => {
tips.show = false
}, 3000)
const clearForm = () => {
form.username = ''
form.password = ''
form.agreement = true
errors.username = ''
errors.password = ''
errors.agreement = ''
showToast('已清空输入')
}
// 处理登录
const handleLogin = async () => {
// 校验表单
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);
}
.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);
.login-shell {
max-width: 460px;
}
}
</style>

View File

@@ -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>

View File

@@ -5,9 +5,5 @@
</template>
<script setup lang="ts">
const router = useRouter()
onMounted(() => {
router.replace('/home')
})
await navigateTo('/auth/login', { replace: true })
</script>

View File

@@ -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)