1465 lines
28 KiB
Markdown
1465 lines
28 KiB
Markdown
# Lyzsys 前端开发文档
|
||
|
||
## 目录
|
||
|
||
- [项目概述](#项目概述)
|
||
- [技术栈](#技术栈)
|
||
- [项目结构](#项目结构)
|
||
- [开发指南](#开发指南)
|
||
- [核心功能](#核心功能)
|
||
- [组件库](#组件库)
|
||
- [样式指南](#样式指南)
|
||
- [部署指南](#部署指南)
|
||
|
||
---
|
||
|
||
## 项目概述
|
||
|
||
Lyzsys 前端是基于 Vue 3 + TypeScript + Element Plus 构建的现代化管理系统,采用 Composition API 和模块化架构,支持多租户、权限管理、国际化等功能。
|
||
|
||
### 核心特性
|
||
|
||
- **Vue 3 Composition API**: 充分利用 Vue 3 的新特性
|
||
- **TypeScript**: 完整的类型支持
|
||
- **Vite 构建**: 极速的开发体验
|
||
- **Element Plus**: 企业级 UI 组件库
|
||
- **权限管理**: 基于 RBAC 的权限控制
|
||
- **国际化**: 支持多语言切换
|
||
- **主题定制**: 灵活的主题配置
|
||
|
||
---
|
||
|
||
## 技术栈
|
||
|
||
### 核心框架
|
||
|
||
| 技术 | 版本 | 说明 |
|
||
|------|------|------|
|
||
| Vue | 3.2+ | 渐进式 JavaScript 框架 |
|
||
| TypeScript | 4.x | JavaScript 的超集 |
|
||
| Vite | 4.x | 下一代前端构建工具 |
|
||
| Vue Router | 4.x | Vue.js 官方路由管理器 |
|
||
| Pinia | 2.x | Vue 官方状态管理库 |
|
||
|
||
### UI 框架
|
||
|
||
| 技术 | 版本 | 说明 |
|
||
|------|------|------|
|
||
| Element Plus | 2.x | 基于 Vue 3 的组件库 |
|
||
| UnoCSS | - | 原子化 CSS 引擎 |
|
||
| Animate.css | - | CSS 动画库 |
|
||
| Iconify | - | 统一的图标框架 |
|
||
|
||
### 工具库
|
||
|
||
| 技术 | 版本 | 说明 |
|
||
|------|------|------|
|
||
| Axios | - | HTTP 客户端 |
|
||
| Day.js | - | 轻量级日期处理库 |
|
||
| Lodash | - | JavaScript 实用工具库 |
|
||
| Crypto-js | - | JavaScript 加密库 |
|
||
| DOMPurify | - | XSS 过滤器 |
|
||
| WangEditor | - | 富文本编辑器 |
|
||
| ECharts | - | 数据可视化库 |
|
||
|
||
### 开发工具
|
||
|
||
| 技术 | 版本 | 说明 |
|
||
|------|------|------|
|
||
| ESLint | - | 代码检查工具 |
|
||
| Prettier | - | 代码格式化工具 |
|
||
| Stylelint | - | CSS 代码检查工具 |
|
||
| Commitlint | - | Git 提交信息检查 |
|
||
| unplugin-auto-import | - | 自动导入 API |
|
||
| unplugin-vue-components | - | 自动导入组件 |
|
||
|
||
---
|
||
|
||
## 项目结构
|
||
|
||
### 目录结构
|
||
|
||
```
|
||
lyzsys-ui-admin/
|
||
├── .github/ # GitHub Actions 配置
|
||
├── .husky/ # Git Hooks 配置
|
||
├── .vscode/ # VSCode 配置
|
||
├── public/ # 静态资源
|
||
├── src/ # 源代码
|
||
│ ├── api/ # API 接口管理
|
||
│ │ ├── ai/ # AI 相关接口
|
||
│ │ ├── bpm/ # 工作流接口
|
||
│ │ ├── crm/ # CRM 接口
|
||
│ │ ├── erp/ # ERP 接口
|
||
│ │ ├── system/ # 系统管理接口
|
||
│ │ └── login/ # 登录接口
|
||
│ ├── assets/ # 静态资源
|
||
│ │ ├── images/ # 图片
|
||
│ │ ├── styles/ # 样式文件
|
||
│ │ └── svg/ # SVG 图标
|
||
│ ├── components/ # 全局组件
|
||
│ ├── config/ # 全局配置
|
||
│ ├── directives/ # 自定义指令
|
||
│ ├── hooks/ # 组合式函数
|
||
│ ├── layout/ # 布局组件
|
||
│ ├── locales/ # 国际化文件
|
||
│ │ ├── en/ # 英文
|
||
│ │ └── zh-CN/ # 简体中文
|
||
│ ├── plugins/ # 插件配置
|
||
│ ├── router/ # 路由配置
|
||
│ ├── store/ # 状态管理
|
||
│ │ ├── modules/ # 模块化 Store
|
||
│ │ └── index.ts # Store 入口
|
||
│ ├── styles/ # 全局样式
|
||
│ ├── types/ # TypeScript 类型定义
|
||
│ ├── utils/ # 工具函数
|
||
│ ├── views/ # 页面组件
|
||
│ ├── App.vue # 应用根组件
|
||
│ ├── main.ts # 应用入口
|
||
│ └── permission.ts # 路由权限控制
|
||
├── types/ # 全局类型定义
|
||
├── .env.base # 基础环境变量
|
||
├── .env.dev # 开发环境变量
|
||
├── .env.pro # 生产环境变量
|
||
├── .env.test # 测试环境变量
|
||
├── .eslintrc.js # ESLint 配置
|
||
├── .gitignore # Git 忽略文件
|
||
├── .prettierrc.js # Prettier 配置
|
||
├── index.html # HTML 模板
|
||
├── package.json # 项目依赖
|
||
├── tsconfig.json # TypeScript 配置
|
||
└── vite.config.ts # Vite 配置
|
||
```
|
||
|
||
### 核心文件说明
|
||
|
||
#### main.ts
|
||
|
||
应用入口文件,负责初始化 Vue 应用:
|
||
|
||
```typescript
|
||
import { createApp } from 'vue'
|
||
import App from './App.vue'
|
||
import router from './router'
|
||
import { setupStore } from './store'
|
||
import { setupRouter } from './router'
|
||
import { setupElPlus, setupIcons } from './plugins'
|
||
|
||
async function bootstrap() {
|
||
const app = createApp(App)
|
||
|
||
// 配置 Store
|
||
setupStore(app)
|
||
|
||
// 配置 Router
|
||
await setupRouter(app)
|
||
|
||
// 配置插件
|
||
setupElPlus(app)
|
||
setupIcons(app)
|
||
|
||
// 挂载应用
|
||
app.mount('#app')
|
||
}
|
||
|
||
bootstrap()
|
||
```
|
||
|
||
#### App.vue
|
||
|
||
应用根组件:
|
||
|
||
```vue
|
||
<template>
|
||
<ConfigProvider :locale="getLocale">
|
||
<router-view />
|
||
</ConfigProvider>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ConfigProvider } from 'element-plus'
|
||
import { useLocale } from './locales/useLocale'
|
||
import { getLocale } from './locales/getLocale'
|
||
</script>
|
||
```
|
||
|
||
#### permission.ts
|
||
|
||
路由权限控制:
|
||
|
||
```typescript
|
||
import router from './router'
|
||
import { useUserStore } from './store/modules/user'
|
||
import { usePermissionStore } from './store/modules/permission'
|
||
|
||
router.beforeEach(async (to, from, next) => {
|
||
const userStore = useUserStore()
|
||
const permissionStore = usePermissionStore()
|
||
|
||
// 检查是否登录
|
||
if (userStore.getToken) {
|
||
// 已登录
|
||
if (to.path === '/login') {
|
||
next({ path: '/' })
|
||
} else {
|
||
// 检查是否已获取权限
|
||
if (permissionStore.getIsAddRouters) {
|
||
next()
|
||
} else {
|
||
// 获取权限和路由
|
||
await permissionStore.generateRoutes()
|
||
next({ ...to, replace: true })
|
||
}
|
||
}
|
||
} else {
|
||
// 未登录
|
||
if (to.path === '/login') {
|
||
next()
|
||
} else {
|
||
next(`/login?redirect=${to.path}`)
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 开发指南
|
||
|
||
### 环境准备
|
||
|
||
#### 必需软件
|
||
|
||
| 软件 | 版本要求 | 下载地址 |
|
||
|------|----------|----------|
|
||
| Node.js | 16+ | https://nodejs.org/ |
|
||
| pnpm | 8+ | https://pnpm.io/ |
|
||
| VSCode | 最新版 | https://code.visualstudio.com/ |
|
||
|
||
#### 推荐插件
|
||
|
||
- Vue - Official
|
||
- TypeScript Vue Plugin (Volar)
|
||
- ESLint
|
||
- Prettier - Code formatter
|
||
- Stylelint
|
||
- UnoCSS
|
||
|
||
### 快速开始
|
||
|
||
#### 1. 克隆项目
|
||
|
||
```bash
|
||
git clone https://github.com/your-org/lyzsys.git
|
||
cd lyzsys/lyzsys-ui-admin
|
||
```
|
||
|
||
#### 2. 安装依赖
|
||
|
||
```bash
|
||
pnpm install
|
||
```
|
||
|
||
#### 3. 启动开发服务器
|
||
|
||
```bash
|
||
pnpm dev
|
||
```
|
||
|
||
#### 4. 访问应用
|
||
|
||
- 应用地址: http://localhost:5173
|
||
- 用户名: admin
|
||
- 密码: admin123
|
||
|
||
### 开发规范
|
||
|
||
#### 命名规范
|
||
|
||
**文件命名**:
|
||
- 组件文件:大驼峰 `UserList.vue`
|
||
- 工具文件:小驼峰 `formatDate.ts`
|
||
- 样式文件:小驼峰 `userList.scss`
|
||
|
||
**变量命名**:
|
||
```typescript
|
||
// 常量:全大写
|
||
const MAX_SIZE = 100
|
||
|
||
// 变量:小驼峰
|
||
const userName = 'admin'
|
||
|
||
// 接口:大驼峰
|
||
interface UserInfo {
|
||
id: number
|
||
name: string
|
||
}
|
||
|
||
// 类型:大驼峰
|
||
type UserRole = 'admin' | 'user'
|
||
|
||
// 枚举:大驼峰
|
||
enum UserStatus {
|
||
Active = 'active',
|
||
Inactive = 'inactive'
|
||
}
|
||
```
|
||
|
||
**组件命名**:
|
||
```vue
|
||
<script setup lang="ts">
|
||
// 组件名:大驼峰
|
||
defineOptions({
|
||
name: 'UserList'
|
||
})
|
||
</script>
|
||
```
|
||
|
||
#### 注释规范
|
||
|
||
**函数注释**:
|
||
```typescript
|
||
/**
|
||
* 获取用户列表
|
||
* @param params 查询参数
|
||
* @returns 用户列表
|
||
*/
|
||
async function getUserList(params: UserPageReqVO) {
|
||
return await userApi.getUserPage(params)
|
||
}
|
||
```
|
||
|
||
**组件注释**:
|
||
```vue
|
||
<!--
|
||
@description 用户列表组件
|
||
@author Lyzsys Team
|
||
-->
|
||
<template>
|
||
<!-- ... -->
|
||
</template>
|
||
```
|
||
|
||
#### 代码风格
|
||
|
||
**使用 Composition API**:
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
|
||
// 响应式数据
|
||
const loading = ref(false)
|
||
const userList = ref<User[]>([])
|
||
|
||
// 计算属性
|
||
const totalCount = computed(() => userList.value.length)
|
||
|
||
// 方法
|
||
async function fetchUsers() {
|
||
loading.value = true
|
||
try {
|
||
userList.value = await userApi.getUserList()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 生命周期
|
||
onMounted(() => {
|
||
fetchUsers()
|
||
})
|
||
</script>
|
||
```
|
||
|
||
**使用 TypeScript**:
|
||
```typescript
|
||
interface User {
|
||
id: number
|
||
name: string
|
||
email: string
|
||
}
|
||
|
||
type UserRole = 'admin' | 'user' | 'guest'
|
||
|
||
const user: User = {
|
||
id: 1,
|
||
name: 'Admin',
|
||
email: 'admin@example.com'
|
||
}
|
||
|
||
const role: UserRole = 'admin'
|
||
```
|
||
|
||
### 组件开发
|
||
|
||
#### 创建组件
|
||
|
||
```vue
|
||
<!-- components/UserList.vue -->
|
||
<template>
|
||
<div class="user-list">
|
||
<el-table :data="userList" :loading="loading">
|
||
<el-table-column prop="name" label="姓名" />
|
||
<el-table-column prop="email" label="邮箱" />
|
||
<el-table-column label="操作">
|
||
<template #default="{ row }">
|
||
<el-button link @click="handleEdit(row)">编辑</el-button>
|
||
<el-button link @click="handleDelete(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
|
||
interface User {
|
||
id: number
|
||
name: string
|
||
email: string
|
||
}
|
||
|
||
defineOptions({
|
||
name: 'UserList'
|
||
})
|
||
|
||
const loading = ref(false)
|
||
const userList = ref<User[]>([])
|
||
|
||
async function fetchUsers() {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await userApi.getUserList()
|
||
userList.value = data
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleEdit(user: User) {
|
||
console.log('编辑用户', user)
|
||
}
|
||
|
||
function handleDelete(user: User) {
|
||
console.log('删除用户', user)
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchUsers()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.user-list {
|
||
padding: 20px;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### 组件通信
|
||
|
||
**Props**:
|
||
```vue
|
||
<script setup lang="ts">
|
||
interface Props {
|
||
userId: number
|
||
userName?: string
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
userName: ''
|
||
})
|
||
</script>
|
||
```
|
||
|
||
**Emits**:
|
||
```vue
|
||
<script setup lang="ts">
|
||
interface Emits {
|
||
(e: 'update:modelValue', value: string): void
|
||
(e: 'change', value: string): void
|
||
}
|
||
|
||
const emit = defineEmits<Emits>()
|
||
|
||
function handleChange(value: string) {
|
||
emit('update:modelValue', value)
|
||
emit('change', value)
|
||
}
|
||
</script>
|
||
```
|
||
|
||
**Slots**:
|
||
```vue
|
||
<template>
|
||
<div class="card">
|
||
<slot name="header">
|
||
<h3>默认标题</h3>
|
||
</slot>
|
||
<slot>
|
||
默认内容
|
||
</slot>
|
||
<slot name="footer">
|
||
<el-button>确定</el-button>
|
||
</slot>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
### API 开发
|
||
|
||
#### API 定义
|
||
|
||
```typescript
|
||
// api/system/user/index.ts
|
||
import request from '@/utils/request'
|
||
|
||
// 获取用户分页
|
||
export function getUserPage(params: UserPageReqVO) {
|
||
return request.get({ url: '/system/user/page', params })
|
||
}
|
||
|
||
// 获取用户详情
|
||
export function getUser(id: number) {
|
||
return request.get({ url: `/system/user/get?id=${id}` })
|
||
}
|
||
|
||
// 创建用户
|
||
export function createUser(data: UserCreateReqVO) {
|
||
return request.post({ url: '/system/user/create', data })
|
||
}
|
||
|
||
// 更新用户
|
||
export function updateUser(data: UserUpdateReqVO) {
|
||
return request.put({ url: '/system/user/update', data })
|
||
}
|
||
|
||
// 删除用户
|
||
export function deleteUser(id: number) {
|
||
return request.delete({ url: `/system/user/delete?id=${id}` })
|
||
}
|
||
```
|
||
|
||
#### API 调用
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { getUserPage } from '@/api/system/user'
|
||
|
||
const loading = ref(false)
|
||
const userList = ref([])
|
||
|
||
async function fetchUsers() {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await getUserPage({
|
||
pageNo: 1,
|
||
pageSize: 10
|
||
})
|
||
userList.value = data.list
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 路由开发
|
||
|
||
#### 路由配置
|
||
|
||
```typescript
|
||
// router/modules/system.ts
|
||
import type { AppRouteModule } from '@/router/types'
|
||
|
||
const system: AppRouteModule = {
|
||
path: '/system',
|
||
name: 'System',
|
||
component: 'LAYOUT',
|
||
redirect: '/system/user',
|
||
meta: {
|
||
title: '系统管理',
|
||
icon: 'system',
|
||
orderNo: 1000
|
||
},
|
||
children: [
|
||
{
|
||
path: 'user',
|
||
name: 'SystemUser',
|
||
component: () => import('@/views/system/user/index.vue'),
|
||
meta: {
|
||
title: '用户管理',
|
||
icon: 'user',
|
||
noCache: true
|
||
}
|
||
},
|
||
{
|
||
path: 'role',
|
||
name: 'SystemRole',
|
||
component: () => import('@/views/system/role/index.vue'),
|
||
meta: {
|
||
title: '角色管理',
|
||
icon: 'role',
|
||
noCache: true
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
export default system
|
||
```
|
||
|
||
#### 路由跳转
|
||
|
||
```typescript
|
||
import { useRouter } from 'vue-router'
|
||
|
||
const router = useRouter()
|
||
|
||
// 路径跳转
|
||
router.push('/system/user')
|
||
|
||
// 命名路由跳转
|
||
router.push({ name: 'SystemUser' })
|
||
|
||
// 带参数跳转
|
||
router.push({
|
||
path: '/system/user',
|
||
query: { id: 1 }
|
||
})
|
||
|
||
// 替换当前路由
|
||
router.replace('/system/user')
|
||
|
||
// 后退
|
||
router.back()
|
||
|
||
// 前进
|
||
router.forward()
|
||
```
|
||
|
||
### 状态管理
|
||
|
||
#### Store 定义
|
||
|
||
```typescript
|
||
// store/modules/user.ts
|
||
import { defineStore } from 'pinia'
|
||
import { getUserInfo, login } from '@/api/system/user'
|
||
|
||
interface UserState {
|
||
token: string
|
||
userInfo: UserInfo | null
|
||
}
|
||
|
||
export const useUserStore = defineStore('user', {
|
||
state: (): UserState => ({
|
||
token: '',
|
||
userInfo: null
|
||
}),
|
||
|
||
getters: {
|
||
getToken: (state) => state.token,
|
||
getUserInfo: (state) => state.userInfo
|
||
},
|
||
|
||
actions: {
|
||
setToken(token: string) {
|
||
this.token = token
|
||
},
|
||
|
||
async login(params: LoginReqVO) {
|
||
const { data } = await login(params)
|
||
this.setToken(data.token)
|
||
},
|
||
|
||
async getUserInfoAction() {
|
||
const { data } = await getUserInfo()
|
||
this.userInfo = data
|
||
},
|
||
|
||
async logout() {
|
||
this.token = ''
|
||
this.userInfo = null
|
||
}
|
||
},
|
||
|
||
persist: {
|
||
key: 'user-store',
|
||
storage: localStorage
|
||
}
|
||
})
|
||
```
|
||
|
||
#### Store 使用
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { useUserStore } from '@/store/modules/user'
|
||
|
||
const userStore = useUserStore()
|
||
|
||
// 访问 state
|
||
console.log(userStore.token)
|
||
|
||
// 访问 getters
|
||
console.log(userStore.getToken)
|
||
|
||
// 调用 actions
|
||
await userStore.login({
|
||
username: 'admin',
|
||
password: 'admin123'
|
||
})
|
||
|
||
// 重置 state
|
||
userStore.$reset()
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 核心功能
|
||
|
||
### 权限管理
|
||
|
||
#### 权限指令
|
||
|
||
```vue
|
||
<template>
|
||
<!-- 有权限时显示 -->
|
||
<el-button v-hasPermi="['system:user:create']">创建用户</el-button>
|
||
|
||
<!-- 有角色时显示 -->
|
||
<el-button v-hasRole="['admin']">管理员操作</el-button>
|
||
|
||
<!-- 同时满足多个权限 -->
|
||
<el-button v-hasPermi="['system:user:update', 'system:user:delete']">
|
||
操作
|
||
</el-button>
|
||
</template>
|
||
```
|
||
|
||
#### 权限函数
|
||
|
||
```typescript
|
||
import { usePermission } from '@/hooks/web/usePermission'
|
||
|
||
const { hasPermission, hasRole } = usePermission()
|
||
|
||
// 检查权限
|
||
if (hasPermission(['system:user:create'])) {
|
||
// 有权限
|
||
}
|
||
|
||
// 检查角色
|
||
if (hasRole(['admin'])) {
|
||
// 是管理员
|
||
}
|
||
```
|
||
|
||
### 国际化
|
||
|
||
#### 使用翻译
|
||
|
||
```vue
|
||
<template>
|
||
<!-- 使用翻译 -->
|
||
<h1>{{ $t('common.title') }}</h1>
|
||
|
||
<!-- 带参数的翻译 -->
|
||
<p>{{ $t('common.welcome', { name: 'Admin' }) }}</p>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { useI18n } from '@/hooks/web/useI18n'
|
||
|
||
const { t } = useI18n()
|
||
|
||
// 使用翻译
|
||
const message = t('common.title')
|
||
const welcome = t('common.welcome', { name: 'Admin' })
|
||
</script>
|
||
```
|
||
|
||
#### 添加翻译
|
||
|
||
```typescript
|
||
// locales/zh-CN/common.ts
|
||
export default {
|
||
title: 'Lyzsys 管理系统',
|
||
welcome: '欢迎,{name}',
|
||
logout: '退出登录'
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// locales/en/common.ts
|
||
export default {
|
||
title: 'Lyzsys Admin',
|
||
welcome: 'Welcome, {name}',
|
||
logout: 'Logout'
|
||
}
|
||
```
|
||
|
||
### 主题定制
|
||
|
||
#### 修改主题色
|
||
|
||
```typescript
|
||
// styles/variables.scss
|
||
$primary-color: #409eff;
|
||
$success-color: #67c23a;
|
||
$warning-color: #e6a23c;
|
||
$danger-color: #f56c6c;
|
||
$error-color: #f56c6c;
|
||
```
|
||
|
||
#### 动态切换主题
|
||
|
||
```vue
|
||
<script setup lang="ts">
|
||
import { useAppStore } from '@/store/modules/app'
|
||
|
||
const appStore = useAppStore()
|
||
|
||
// 切换暗黑模式
|
||
function toggleDarkMode() {
|
||
appStore.setDarkMode(!appStore.getDarkMode)
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 文件上传
|
||
|
||
#### 单文件上传
|
||
|
||
```vue
|
||
<template>
|
||
<el-upload
|
||
:action="uploadUrl"
|
||
:headers="uploadHeaders"
|
||
:on-success="handleSuccess"
|
||
:before-upload="beforeUpload"
|
||
>
|
||
<el-button type="primary">上传文件</el-button>
|
||
</el-upload>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getAccessToken } from '@/utils/auth'
|
||
|
||
const uploadUrl = import.meta.env.VITE_BASE_URL + '/admin-api/infra/file/upload'
|
||
const uploadHeaders = ref({
|
||
Authorization: 'Bearer ' + getAccessToken()
|
||
})
|
||
|
||
function beforeUpload(file: File) {
|
||
const isLt2M = file.size / 1024 / 1024 < 2
|
||
if (!isLt2M) {
|
||
ElMessage.error('上传文件大小不能超过 2MB!')
|
||
}
|
||
return isLt2M
|
||
}
|
||
|
||
function handleSuccess(response: any) {
|
||
if (response.code === 0) {
|
||
ElMessage.success('上传成功')
|
||
console.log('文件 URL:', response.data)
|
||
} else {
|
||
ElMessage.error(response.msg)
|
||
}
|
||
}
|
||
</script>
|
||
```
|
||
|
||
#### 多文件上传
|
||
|
||
```vue
|
||
<template>
|
||
<el-upload
|
||
:action="uploadUrl"
|
||
:headers="uploadHeaders"
|
||
multiple
|
||
:limit="3"
|
||
:on-exceed="handleExceed"
|
||
:on-success="handleSuccess"
|
||
>
|
||
<el-button type="primary">多文件上传</el-button>
|
||
</el-upload>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
function handleExceed() {
|
||
ElMessage.warning('最多只能上传 3 个文件')
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 表单验证
|
||
|
||
#### 表单定义
|
||
|
||
```vue
|
||
<template>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
label-width="100px"
|
||
>
|
||
<el-form-item label="用户名" prop="username">
|
||
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="邮箱" prop="email">
|
||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="密码" prop="password">
|
||
<el-input
|
||
v-model="form.password"
|
||
type="password"
|
||
placeholder="请输入密码"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||
<el-button @click="handleReset">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive } from 'vue'
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
|
||
const formRef = ref<FormInstance>()
|
||
|
||
const form = reactive({
|
||
username: '',
|
||
email: '',
|
||
password: ''
|
||
})
|
||
|
||
const rules: FormRules = {
|
||
username: [
|
||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
|
||
],
|
||
email: [
|
||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
|
||
],
|
||
password: [
|
||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
if (!formRef.value) return
|
||
|
||
await formRef.value.validate((valid) => {
|
||
if (valid) {
|
||
console.log('表单数据:', form)
|
||
// 提交表单
|
||
}
|
||
})
|
||
}
|
||
|
||
function handleReset() {
|
||
formRef.value?.resetFields()
|
||
}
|
||
</script>
|
||
```
|
||
|
||
### 表格操作
|
||
|
||
#### 基础表格
|
||
|
||
```vue
|
||
<template>
|
||
<el-table :data="tableData" :loading="loading" border>
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column prop="username" label="用户名" />
|
||
<el-table-column prop="email" label="邮箱" />
|
||
<el-table-column prop="status" label="状态">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 'active' ? 'success' : 'danger'">
|
||
{{ row.status === 'active' ? '激活' : '禁用' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="200" fixed="right">
|
||
<template #default="{ row }">
|
||
<el-button link @click="handleEdit(row)">编辑</el-button>
|
||
<el-button link @click="handleDelete(row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from 'vue'
|
||
|
||
interface User {
|
||
id: number
|
||
username: string
|
||
email: string
|
||
status: string
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const tableData = ref<User[]>([])
|
||
|
||
function handleEdit(user: User) {
|
||
console.log('编辑用户', user)
|
||
}
|
||
|
||
function handleDelete(user: User) {
|
||
console.log('删除用户', user)
|
||
}
|
||
</script>
|
||
```
|
||
|
||
#### 分页表格
|
||
|
||
```vue
|
||
<template>
|
||
<div>
|
||
<el-table :data="tableData" :loading="loading" border>
|
||
<!-- 表格列 -->
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
v-model:current-page="queryParams.pageNo"
|
||
v-model:page-size="queryParams.pageSize"
|
||
:total="total"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
@size-change="handleSizeChange"
|
||
@current-change="handleCurrentChange"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive } from 'vue'
|
||
|
||
const loading = ref(false)
|
||
const tableData = ref([])
|
||
const total = ref(0)
|
||
|
||
const queryParams = reactive({
|
||
pageNo: 1,
|
||
pageSize: 10
|
||
})
|
||
|
||
async function fetchTableData() {
|
||
loading.value = true
|
||
try {
|
||
const { data } = await userApi.getUserPage(queryParams)
|
||
tableData.value = data.list
|
||
total.value = data.total
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSizeChange() {
|
||
fetchTableData()
|
||
}
|
||
|
||
function handleCurrentChange() {
|
||
fetchTableData()
|
||
}
|
||
|
||
// 初始加载
|
||
fetchTableData()
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 组件库
|
||
|
||
### Element Plus
|
||
|
||
#### 按钮
|
||
|
||
```vue
|
||
<el-button>默认按钮</el-button>
|
||
<el-button type="primary">主要按钮</el-button>
|
||
<el-button type="success">成功按钮</el-button>
|
||
<el-button type="warning">警告按钮</el-button>
|
||
<el-button type="danger">危险按钮</el-button>
|
||
<el-button link>文字按钮</el-button>
|
||
```
|
||
|
||
#### 表单
|
||
|
||
```vue
|
||
<el-input v-model="value" placeholder="请输入内容" />
|
||
<el-input v-model="value" type="textarea" />
|
||
<el-input v-model="value" type="password" />
|
||
```
|
||
|
||
#### 选择器
|
||
|
||
```vue
|
||
<el-select v-model="value" placeholder="请选择">
|
||
<el-option label="选项1" value="1" />
|
||
<el-option label="选项2" value="2" />
|
||
</el-select>
|
||
```
|
||
|
||
#### 日期选择器
|
||
|
||
```vue
|
||
<el-date-picker
|
||
v-model="value"
|
||
type="date"
|
||
placeholder="选择日期"
|
||
/>
|
||
|
||
<el-date-picker
|
||
v-model="value"
|
||
type="daterange"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/>
|
||
```
|
||
|
||
#### 对话框
|
||
|
||
```vue
|
||
<el-dialog v-model="visible" title="提示" width="30%">
|
||
<span>这是一段内容</span>
|
||
<template #footer>
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button type="primary" @click="visible = false">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
```
|
||
|
||
#### 消息提示
|
||
|
||
```typescript
|
||
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
|
||
|
||
// 消息提示
|
||
ElMessage.success('操作成功')
|
||
ElMessage.warning('警告消息')
|
||
ElMessage.error('错误消息')
|
||
ElMessage.info('提示消息')
|
||
|
||
// 确认框
|
||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(() => {
|
||
ElMessage.success('删除成功')
|
||
})
|
||
.catch(() => {
|
||
ElMessage.info('已取消删除')
|
||
})
|
||
|
||
// 通知
|
||
ElNotification({
|
||
title: '通知',
|
||
message: '这是一条通知消息',
|
||
type: 'success'
|
||
})
|
||
```
|
||
|
||
### 自定义组件
|
||
|
||
#### 图表组件 (ECharts)
|
||
|
||
```vue
|
||
<template>
|
||
<div ref="chartRef" style="width: 100%; height: 400px"></div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import * as echarts from 'echarts'
|
||
import type { EChartsOption } from 'echarts'
|
||
|
||
const chartRef = ref<HTMLDivElement>()
|
||
let chartInstance: echarts.ECharts | null = null
|
||
|
||
onMounted(() => {
|
||
if (chartRef.value) {
|
||
chartInstance = echarts.init(chartRef.value)
|
||
|
||
const option: EChartsOption = {
|
||
title: {
|
||
text: '示例图表'
|
||
},
|
||
xAxis: {
|
||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||
},
|
||
yAxis: {},
|
||
series: [
|
||
{
|
||
type: 'line',
|
||
data: [150, 230, 224, 218, 135, 147, 260]
|
||
}
|
||
]
|
||
}
|
||
|
||
chartInstance.setOption(option)
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
chartInstance?.dispose()
|
||
})
|
||
</script>
|
||
```
|
||
|
||
#### 富文本编辑器 (WangEditor)
|
||
|
||
```vue
|
||
<template>
|
||
<div ref="editorRef" style="height: 500px"></div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted } from 'vue'
|
||
import { createEditor, createToolbar } from '@wangeditor/editor-for-vue'
|
||
|
||
const editorRef = ref<HTMLDivElement>()
|
||
let editor: any = null
|
||
let toolbar: any = null
|
||
|
||
onMounted(() => {
|
||
if (editorRef.value) {
|
||
editor = createEditor({
|
||
selector: editorRef.value,
|
||
html: '<p>默认内容</p>',
|
||
config: {}
|
||
})
|
||
|
||
toolbar = createToolbar({
|
||
editor,
|
||
config: {}
|
||
})
|
||
}
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
editor?.destroy()
|
||
toolbar?.destroy()
|
||
})
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 样式指南
|
||
|
||
### CSS 预处理器
|
||
|
||
使用 SCSS 作为 CSS 预处理器:
|
||
|
||
```scss
|
||
// 使用变量
|
||
$primary-color: #409eff;
|
||
$border-radius: 4px;
|
||
|
||
// 使用嵌套
|
||
.button {
|
||
background: $primary-color;
|
||
border-radius: $border-radius;
|
||
|
||
&:hover {
|
||
background: darken($primary-color, 10%);
|
||
}
|
||
|
||
&.active {
|
||
background: lighten($primary-color, 10%);
|
||
}
|
||
}
|
||
```
|
||
|
||
### UnoCSS
|
||
|
||
使用原子化 CSS:
|
||
|
||
```vue
|
||
<template>
|
||
<div class="flex items-center justify-center h-screen bg-gray-100">
|
||
<div class="p-6 bg-white rounded-lg shadow-lg">
|
||
<h1 class="text-2xl font-bold text-gray-800">标题</h1>
|
||
<p class="mt-4 text-gray-600">内容</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
### 响应式设计
|
||
|
||
```scss
|
||
// 使用媒体查询
|
||
.container {
|
||
padding: 20px;
|
||
|
||
@media (max-width: 768px) {
|
||
padding: 10px;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
padding: 5px;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 主题定制
|
||
|
||
```scss
|
||
// 自定义主题色
|
||
:root {
|
||
--el-color-primary: #409eff;
|
||
--el-color-success: #67c23a;
|
||
--el-color-warning: #e6a23c;
|
||
--el-color-danger: #f56c6c;
|
||
}
|
||
|
||
// 暗黑模式
|
||
.dark {
|
||
--el-bg-color: #1a1a1a;
|
||
--el-text-color-primary: #ffffff;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 部署指南
|
||
|
||
### 本地构建
|
||
|
||
```bash
|
||
# 开发环境构建
|
||
pnpm build:dev
|
||
|
||
# 生产环境构建
|
||
pnpm build:prod
|
||
|
||
# 测试环境构建
|
||
pnpm build:test
|
||
```
|
||
|
||
### 环境变量配置
|
||
|
||
```bash
|
||
# .env.base
|
||
VITE_BASE_URL=/api
|
||
|
||
# .env.dev
|
||
VITE_BASE_URL=http://localhost:48080
|
||
|
||
# .env.prod
|
||
VITE_BASE_URL=https://api.example.com
|
||
```
|
||
|
||
### Docker 部署
|
||
|
||
#### 1. 构建镜像
|
||
|
||
```bash
|
||
docker build -t lyzsys/lyzsys-ui-admin:latest .
|
||
```
|
||
|
||
#### 2. 运行容器
|
||
|
||
```bash
|
||
docker run -d \
|
||
--name lyzsys-ui-admin \
|
||
-p 80:80 \
|
||
lyzsys/lyzsys-ui-admin:latest
|
||
```
|
||
|
||
### Nginx 配置
|
||
|
||
```nginx
|
||
server {
|
||
listen 80;
|
||
server_name example.com;
|
||
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
|
||
location /api {
|
||
proxy_pass http://backend:48080;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
}
|
||
|
||
gzip on;
|
||
gzip_types text/plain text/css application/json application/javascript;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录
|
||
|
||
### 常见问题
|
||
|
||
#### Q1: 如何添加新页面?
|
||
|
||
1. 在 `views/` 目录下创建页面组件
|
||
2. 在 `router/modules/` 中配置路由
|
||
3. 添加国际化翻译(如需要)
|
||
4. 在后台系统中配置菜单权限
|
||
|
||
#### Q2: 如何调用后端 API?
|
||
|
||
1. 在 `api/` 目录下创建 API 文件
|
||
2. 定义 API 接口函数
|
||
3. 在组件中导入并调用
|
||
4. 处理响应数据
|
||
|
||
#### Q3: 如何实现权限控制?
|
||
|
||
1. 使用 `v-hasPermi` 指令控制按钮显示
|
||
2. 使用 `v-hasRole` 指令控制角色访问
|
||
3. 在路由配置中设置权限 meta
|
||
4. 后端接口进行权限验证
|
||
|
||
#### Q4: 如何切换语言?
|
||
|
||
1. 在 `locales/` 目录下添加语言文件
|
||
2. 使用 `$t()` 函数进行翻译
|
||
3. 在语言切换器中调用 `setLocale()` 方法
|
||
|
||
### 参考资源
|
||
|
||
- [Vue 3 官方文档](https://cn.vuejs.org/)
|
||
- [TypeScript 官方文档](https://www.typescriptlang.org/zh/)
|
||
- [Vite 官方文档](https://cn.vitejs.dev/)
|
||
- [Element Plus 官方文档](https://element-plus.org/zh-CN/)
|
||
- [Pinia 官方文档](https://pinia.vuejs.org/zh/)
|
||
- [UnoCSS 官方文档](https://unocss.dev/)
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0.0
|
||
**最后更新**: 2025-01-19
|
||
**维护团队**: Lyzsys 开发团队
|