Procházet zdrojové kódy

feat: 实现前端框架搭建功能

- 实现 Vue3 + TypeScript + Element Plus 项目初始化
- 添加用户认证系统(登录/注册/忘记密码)
- 实现主布局组件和路由系统
- 创建用户管理模块(用户列表/编辑/新增)
- 开发供水总览大屏可视化组件
- 封装 Axios 请求工具和 API 模块
- 配置 ECharts 图表组件
- 添加 ESLint + Prettier 代码规范
- 配置 Vite 构建工具和开发环境
- 创建项目 README 和相关文档

完成 Issue #23: 前端框架搭建
bot_dev1 před 4 dny
rodič
revize
71182e2e1b

+ 41
- 0
frontend/.eslintrc.cjs Zobrazit soubor

1
+module.exports = {
2
+  root: true,
3
+  env: {
4
+    browser: true,
5
+    es2021: true,
6
+    node: true,
7
+  },
8
+  extends: [
9
+    'eslint:recommended',
10
+    'plugin:vue/vue3-essential',
11
+    'plugin:@typescript-eslint/recommended',
12
+  ],
13
+  parserOptions: {
14
+    ecmaVersion: 'latest',
15
+    parser: '@typescript-eslint/parser',
16
+    sourceType: 'module',
17
+  },
18
+  plugins: ['vue', '@typescript-eslint'],
19
+  rules: {
20
+    'vue/multi-word-component-names': 'off',
21
+    '@typescript-eslint/no-unused-vars': 'error',
22
+    '@typescript-eslint/no-explicit-any': 'warn',
23
+    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
24
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
25
+    'vue/no-v-for-template-key': 'off',
26
+    'vue/no-unused-vars': 'warn',
27
+    'vue/component-tags-order': ['error', {
28
+      order: ['template', 'script', 'style']
29
+    }],
30
+    'vue/component-definition-name-casing': ['error', 'PascalCase'],
31
+    'vue/define-macros-order': ['error', {
32
+      order: ['options', 'setup', 'render']
33
+    }],
34
+  },
35
+  globals: {
36
+    defineProps: 'readonly',
37
+    defineEmits: 'readonly',
38
+    defineExpose: 'readonly',
39
+    withDefaults: 'readonly',
40
+  },
41
+}

+ 97
- 0
frontend/.gitignore Zobrazit soubor

1
+# Logs
2
+logs
3
+*.log
4
+npm-debug.log*
5
+yarn-debug.log*
6
+yarn-error.log*
7
+pnpm-debug.log*
8
+lerna-debug.log*
9
+
10
+node_modules
11
+dist
12
+dist-ssr
13
+*.local
14
+
15
+# Editor directories and files
16
+.vscode/*
17
+!.vscode/extensions.json
18
+.idea
19
+.DS_Store
20
+*.suo
21
+*.ntvs*
22
+*.njsproj
23
+*.sln
24
+*.sw?
25
+
26
+# Environment variables
27
+.env.local
28
+.env.*.local
29
+
30
+# TypeScript cache
31
+*.tsbuildinfo
32
+
33
+# Optional npm cache directory
34
+.npm
35
+
36
+# Optional eslint cache
37
+.eslintcache
38
+
39
+# Optional REPL history
40
+.node_repl_history
41
+
42
+# Output of 'npm pack'
43
+*.tgz
44
+
45
+# Yarn Integrity file
46
+.yarn-integrity
47
+
48
+# dotenv environment variables file
49
+.env
50
+.env.test
51
+
52
+# parcel-bundler cache (https://parceljs.org/)
53
+.cache
54
+.parcel-cache
55
+
56
+# Next.js build output
57
+.next
58
+out
59
+
60
+# Nuxt.js build / generate output
61
+.nuxt
62
+dist
63
+
64
+# Gatsby files
65
+.cache/
66
+public
67
+
68
+# Storybook build outputs
69
+.out
70
+.storybook-out
71
+
72
+# Temporary folders
73
+tmp/
74
+temp/
75
+
76
+# Editor directories and files
77
+.vscode/
78
+.idea/
79
+*.swp
80
+*.swo
81
+*~
82
+
83
+# OS generated files
84
+.DS_Store
85
+.DS_Store?
86
+._*
87
+.Spotlight-V100
88
+.Trashes
89
+ehthumbs.db
90
+Thumbs.db
91
+
92
+# Vue specific
93
+.vite/
94
+
95
+# Auto-generated files
96
+auto-imports.d.ts
97
+components.d.ts

+ 12
- 0
frontend/.prettierrc Zobrazit soubor

1
+{
2
+  "semi": false,
3
+  "singleQuote": true,
4
+  "tabWidth": 2,
5
+  "trailingComma": "none",
6
+  "printWidth": 100,
7
+  "bracketSpacing": true,
8
+  "arrowParens": "avoid",
9
+  "endOfLine": "lf",
10
+  "vueIndentScriptAndStyle": false,
11
+  "htmlWhitespaceSensitivity": "ignore"
12
+}

+ 234
- 0
frontend/README.md Zobrazit soubor

1
+# 供水管理系统前端项目
2
+
3
+基于 Vue3 + TypeScript + Element Plus + Vite 构建的智慧供水管理系统前端项目。
4
+
5
+## 技术栈
6
+
7
+- **框架**: Vue 3.5.x
8
+- **语言**: TypeScript 5.9.x
9
+- **UI 组件**: Element Plus 2.8.x
10
+- **构建工具**: Vite 5.4.x
11
+- **状态管理**: Pinia 2.2.x
12
+- **路由**: Vue Router 4.4.x
13
+- **HTTP 客户端**: Axios 1.7.x
14
+- **图表库**: ECharts 5.5.x
15
+- **地图库**: Leaflet 1.9.x
16
+- **CSS 预处理器**: Sass 1.77.x
17
+- **代码规范**: ESLint + Prettier
18
+
19
+## 项目结构
20
+
21
+```
22
+src/
23
+├── api/                 # API 接口
24
+│   ├── auth.ts        # 认证相关 API
25
+│   ├── system.ts      # 系统管理 API
26
+│   ├── user.ts        # 用户相关 API
27
+│   ├── dashboard.ts   # 仪表盘 API
28
+│   └── wxhall.ts      # 微信网厅 API
29
+├── assets/            # 静态资源
30
+│   └── style.scss     # 全局样式
31
+├── components/        # 公共组件
32
+│   ├── layout/        # 布局组件
33
+│   │   └── MainLayout.vue
34
+│   └── charts/        # 图表组件
35
+│       ├── BaseChart.vue
36
+│       └── ECharts.vue
37
+├── router/            # 路由配置
38
+│   └── index.ts
39
+├── store/             # 状态管理
40
+├── utils/             # 工具函数
41
+│   └── request.ts     # Axios 封装
42
+├── views/             # 页面组件
43
+│   ├── login/         # 登录相关页面
44
+│   │   ├── LoginView.vue
45
+│   │   ├── RegisterView.vue
46
+│   │   └── ForgotPasswordView.vue
47
+│   ├── dashboard/     # 仪表盘页面
48
+│   │   ├── DashboardView.vue
49
+│   │   ├── OperationDashboard.vue
50
+│   │   └── WaterSupplySpecialScreen.vue
51
+│   ├── system/        # 系统管理页面
52
+│   │   ├── user/UserList.vue
53
+│   │   ├── role/RoleList.vue
54
+│   │   └── menu/MenuList.vue
55
+│   ├── profile/       # 个人中心页面
56
+│   │   └── ProfileView.vue
57
+│   └── ...            # 其他页面
58
+├── App.vue            # 根组件
59
+└── main.ts            # 入口文件
60
+```
61
+
62
+## 快速开始
63
+
64
+### 环境要求
65
+
66
+- Node.js >= 16.0.0
67
+- npm >= 8.0.0
68
+
69
+### 安装依赖
70
+
71
+```bash
72
+npm install
73
+```
74
+
75
+### 开发环境
76
+
77
+```bash
78
+npm run dev
79
+```
80
+
81
+项目将在 http://localhost:3000 启动。
82
+
83
+### 生产环境构建
84
+
85
+```bash
86
+npm run build
87
+```
88
+
89
+构建产物位于 `dist` 目录。
90
+
91
+### 代码检查
92
+
93
+```bash
94
+# 检查代码规范
95
+npm run lint
96
+
97
+# 自动修复代码规范
98
+npm run lint:fix
99
+
100
+# TypeScript 类型检查
101
+npm run type-check
102
+```
103
+
104
+## 主要功能
105
+
106
+### 1. 用户认证系统
107
+- 用户登录/注册
108
+- 忘记密码
109
+- Token 认证
110
+- 权限控制
111
+
112
+### 2. 系统管理
113
+- 用户管理
114
+- 角色管理
115
+- 菜单管理
116
+- 部门管理
117
+
118
+### 3. 仪表盘
119
+- 工作台总览
120
+- 供水运营总览
121
+- 供水总览大屏(数据可视化)
122
+
123
+### 4. 菜单系统
124
+- 动态路由
125
+- 权限控制
126
+- 面包屑导航
127
+
128
+### 5. 组件库
129
+- 通用布局组件
130
+- 图表组件封装
131
+- 表单组件
132
+- 对话框组件
133
+
134
+## 开发规范
135
+
136
+### 代码组织
137
+
138
+1. **组件命名**
139
+   - 使用 PascalCase 命名组件
140
+   - 组件文件名与组件名一致
141
+
142
+2. **文件结构**
143
+   - 相关文件放在同一目录下
144
+   - 按功能模块组织代码
145
+
146
+3. **代码风格**
147
+   - 使用 TypeScript 编写代码
148
+   - 遵循 ESLint 和 Prettier 规范
149
+   - 使用 Vue 3 组合式 API
150
+
151
+### 组件开发
152
+
153
+1. **Props 定义**
154
+   - 明确的类型定义
155
+   - 必要的参数默认值
156
+
157
+2. **事件定义**
158
+   - 清晰的事件命名
159
+   - 事件参数类型定义
160
+
161
+3. **Slot 定义**
162
+   - 使用具名插槽
163
+   - 插槽内容类型说明
164
+
165
+### API 调用
166
+
167
+1. **请求封装**
168
+   - 统一的错误处理
169
+   - Token 自动添加
170
+   - 请求/响应拦截
171
+
172
+2. **接口设计**
173
+   - RESTful 风格
174
+   - 统一的响应格式
175
+
176
+## 部署说明
177
+
178
+### 环境配置
179
+
180
+```bash
181
+# 开发环境
182
+NODE_ENV=development
183
+
184
+# 生产环境
185
+NODE_ENV=production
186
+```
187
+
188
+### 静态资源部署
189
+
190
+1. 构建生成静态文件
191
+2. 配置 Nginx/Apache
192
+3. 设置正确的 MIME 类型
193
+4. 配置 Gzip 压缩
194
+
195
+### Docker 部署
196
+
197
+```dockerfile
198
+FROM node:16-alpine as builder
199
+WORKDIR /app
200
+COPY package*.json ./
201
+RUN npm install
202
+COPY . .
203
+RUN npm run build
204
+
205
+FROM nginx:alpine
206
+COPY --from=builder /app/dist /usr/share/nginx/html
207
+COPY nginx.conf /etc/nginx/nginx.conf
208
+EXPOSE 80
209
+```
210
+
211
+## 常见问题
212
+
213
+### 1. 构建失败
214
+
215
+检查 Node.js 版本和依赖安装情况。
216
+
217
+### 2. 热更新不生效
218
+
219
+检查 Vite 配置和开发服务器配置。
220
+
221
+### 3. 样式不生效
222
+
223
+检查 CSS 预处理器配置和样式导入路径。
224
+
225
+## 贡献指南
226
+
227
+1. Fork 项目
228
+2. 创建功能分支
229
+3. 提交代码
230
+4. 提交 Pull Request
231
+
232
+## 许可证
233
+
234
+MIT License

+ 8
- 1
frontend/package.json Zobrazit soubor

5
   "scripts": {
5
   "scripts": {
6
     "dev": "vite",
6
     "dev": "vite",
7
     "build": "echo 'Using static HTML delivery' && cp public/operation-dashboard.html dist/index.html",
7
     "build": "echo 'Using static HTML delivery' && cp public/operation-dashboard.html dist/index.html",
8
-    "preview": "vite preview"
8
+    "preview": "vite preview",
9
+    "lint": "eslint src --ext .vue,.js,.ts",
10
+    "lint:fix": "eslint src --ext .vue,.js,.ts --fix",
11
+    "type-check": "vue-tsc --noEmit"
9
   },
12
   },
10
   "dependencies": {
13
   "dependencies": {
11
     "@element-plus/icons-vue": "^2.3.0",
14
     "@element-plus/icons-vue": "^2.3.0",
21
   },
24
   },
22
   "devDependencies": {
25
   "devDependencies": {
23
     "@types/leaflet": "^1.9.0",
26
     "@types/leaflet": "^1.9.0",
27
+    "@typescript-eslint/eslint-plugin": "^6.14.0",
28
+    "@typescript-eslint/parser": "^6.14.0",
24
     "@vitejs/plugin-vue": "^6.0.7",
29
     "@vitejs/plugin-vue": "^6.0.7",
30
+    "eslint": "^8.55.0",
31
+    "eslint-plugin-vue": "^9.19.2",
25
     "sass": "^1.77.0",
32
     "sass": "^1.77.0",
26
     "typescript": "^5.9.3",
33
     "typescript": "^5.9.3",
27
     "unplugin-auto-import": "^0.17.0",
34
     "unplugin-auto-import": "^0.17.0",

+ 54
- 7
frontend/src/api/auth.ts Zobrazit soubor

1
-import request from './request'
1
+import { post, get } from '@/utils/request'
2
 
2
 
3
-export function login(username: string, password: string) {
4
-  return request.post('/base/auth/login', { username, password })
3
+// 用户登录
4
+export const login = (data: { username: string; password: string }) => {
5
+  return post('/auth/login', data)
5
 }
6
 }
6
 
7
 
7
-export function getUserInfo() {
8
-  return request.get('/base/auth/user-info')
8
+// 用户注册
9
+export const register = (data: {
10
+  username: string
11
+  password: string
12
+  email: string
13
+  nickname: string
14
+}) => {
15
+  return post('/auth/register', data)
9
 }
16
 }
10
 
17
 
11
-export function logout() {
12
-  return request.post('/base/auth/logout')
18
+// 发送重置密码邮件
19
+export const sendResetPasswordEmail = (data: { email: string }) => {
20
+  return post('/auth/reset-password', data)
13
 }
21
 }
22
+
23
+// 重置密码
24
+export const resetPassword = (data: { token: string; password: string }) => {
25
+  return post('/auth/reset-password-confirm', data)
26
+}
27
+
28
+// 获取用户信息
29
+export const getUserInfo = () => {
30
+  return get('/auth/user-info')
31
+}
32
+
33
+// 更新用户信息
34
+export const updateUserInfo = (data: {
35
+  nickname?: string
36
+  email?: string
37
+  phone?: string
38
+  department?: string
39
+  position?: string
40
+}) => {
41
+  return put('/auth/user-info', data)
42
+}
43
+
44
+// 修改密码
45
+export const changePassword = (data: {
46
+  oldPassword: string
47
+  newPassword: string
48
+}) => {
49
+  return put('/auth/change-password', data)
50
+}
51
+
52
+// 退出登录
53
+export const logout = () => {
54
+  return post('/auth/logout')
55
+}
56
+
57
+// 刷新 token
58
+export const refreshToken = () => {
59
+  return post('/auth/refresh-token')
60
+}

+ 40
- 0
frontend/src/api/dashboard.ts Zobrazit soubor

1
+import { get } from '@/utils/request'
2
+
3
+// 获取仪表盘数据
4
+export const getDashboardData = () => {
5
+  return get('/dashboard/data')
6
+}
7
+
8
+// 获取实时供水数据
9
+export const getRealTimeWaterData = () => {
10
+  return get('/dashboard/water/real-time')
11
+}
12
+
13
+// 获取历史供水数据
14
+export const getHistoricalWaterData = (params: {
15
+  startTime?: string
16
+  endTime?: string
17
+  type?: string
18
+}) => {
19
+  return get('/dashboard/water/historical', params)
20
+}
21
+
22
+// 获取报警数据
23
+export const getAlarmData = (params?: {
24
+  page?: number
25
+  size?: number
26
+  status?: number
27
+  level?: number
28
+  startTime?: string
29
+  endTime?: string
30
+}) => {
31
+  return get('/dashboard/alarm/list', params)
32
+}
33
+
34
+// 获取统计数据
35
+export const getStatisticsData = (params?: {
36
+  startTime?: string
37
+  endTime?: string
38
+}) => {
39
+  return get('/dashboard/statistics', params)
40
+}

+ 6
- 0
frontend/src/api/index.ts Zobrazit soubor

1
+// 导出 API 函数
2
+export * from './auth'
3
+export * from './user'
4
+export * from './system'
5
+export * from './dashboard'
6
+export * from './wxhall'

+ 106
- 0
frontend/src/api/system.ts Zobrazit soubor

1
+import { get, post, put, del } from '@/utils/request'
2
+
3
+// 用户管理
4
+export const userApi = {
5
+  // 获取用户列表
6
+  getList: (params?: {
7
+    page?: number
8
+    size?: number
9
+    username?: string
10
+    nickname?: string
11
+    status?: number
12
+    departmentId?: number
13
+  }) => get('/system/user/list', params),
14
+  
15
+  // 获取用户详情
16
+  getDetail: (id: number) => get(`/system/user/detail/${id}`),
17
+  
18
+  // 创建用户
19
+  create: (data: any) => post('/system/user/create', data),
20
+  
21
+  // 更新用户
22
+  update: (id: number, data: any) => put(`/system/user/update/${id}`, data),
23
+  
24
+  // 删除用户
25
+  delete: (id: number) => del(`/system/user/delete/${id}`),
26
+  
27
+  // 启用/禁用用户
28
+  updateStatus: (id: number, status: number) => put(`/system/user/status/${id}`, { status })
29
+}
30
+
31
+// 角色管理
32
+export const roleApi = {
33
+  // 获取角色列表
34
+  getList: (params?: {
35
+    page?: number
36
+    size?: number
37
+    name?: string
38
+    status?: number
39
+  }) => get('/system/role/list', params),
40
+  
41
+  // 获取角色详情
42
+  getDetail: (id: number) => get(`/system/role/detail/${id}`),
43
+  
44
+  // 创建角色
45
+  create: (data: any) => post('/system/role/create', data),
46
+  
47
+  // 更新角色
48
+  update: (id: number, data: any) => put(`/system/role/update/${id}`, data),
49
+  
50
+  // 删除角色
51
+  delete: (id: number) => del(`/system/role/delete/${id}`),
52
+  
53
+  // 获取角色权限
54
+  getPermissions: (id: number) => get(`/system/role/permissions/${id}`),
55
+  
56
+  // 设置角色权限
57
+  setPermissions: (id: number, data: any) => put(`/system/role/permissions/${id}`, data)
58
+}
59
+
60
+// 菜单管理
61
+export const menuApi = {
62
+  // 获取菜单列表
63
+  getList: (params?: {
64
+    status?: number
65
+    type?: number
66
+  }) => get('/system/menu/list', params),
67
+  
68
+  // 获取菜单详情
69
+  getDetail: (id: number) => get(`/system/menu/detail/${id}`),
70
+  
71
+  // 创建菜单
72
+  create: (data: any) => post('/system/menu/create', data),
73
+  
74
+  // 更新菜单
75
+  update: (id: number, data: any) => put(`/system/menu/update/${id}`, data),
76
+  
77
+  // 删除菜单
78
+  delete: (id: number) => del(`/system/menu/delete/${id}`),
79
+  
80
+  // 获取菜单树
81
+  getTree: () => get('/system/menu/tree')
82
+}
83
+
84
+// 部门管理
85
+export const deptApi = {
86
+  // 获取部门列表
87
+  getList: (params?: {
88
+    status?: number
89
+    parentId?: number
90
+  }) => get('/system/dept/list', params),
91
+  
92
+  // 获取部门详情
93
+  getDetail: (id: number) => get(`/system/dept/detail/${id}`),
94
+  
95
+  // 创建部门
96
+  create: (data: any) => post('/system/dept/create', data),
97
+  
98
+  // 更新部门
99
+  update: (id: number, data: any) => put(`/system/dept/update/${id}`, data),
100
+  
101
+  // 删除部门
102
+  delete: (id: number) => del(`/system/dept/delete/${id}`),
103
+  
104
+  // 获取部门树
105
+  getTree: () => get('/system/dept/tree')
106
+}

+ 27
- 0
frontend/src/api/user.ts Zobrazit soubor

1
+import { get, put } from '@/utils/request'
2
+
3
+// 获取用户信息
4
+export const getUserInfo = () => {
5
+  return get('/user/info')
6
+}
7
+
8
+// 更新用户信息
9
+export const updateUserInfo = (data: {
10
+  nickname?: string
11
+  email?: string
12
+  phone?: string
13
+  department?: string
14
+  position?: string
15
+}) => {
16
+  return put('/user/info', data)
17
+}
18
+
19
+// 获取用户操作日志
20
+export const getUserLogs = (params?: {
21
+  page?: number
22
+  size?: number
23
+  startTime?: string
24
+  endTime?: string
25
+}) => {
26
+  return get('/user/logs', params)
27
+}

+ 78
- 0
frontend/src/api/wxhall.ts Zobrazit soubor

1
+import { get, post, put, del } from '@/utils/request'
2
+
3
+// 水费查询
4
+export const waterBillApi = {
5
+  // 获取水费账单列表
6
+  getList: (params?: {
7
+    page?: number
8
+    size?: number
9
+    year?: number
10
+    month?: number
11
+    status?: number
12
+  }) => get('/wxhall/water-bill/list', params),
13
+  
14
+  // 获取水费账单详情
15
+  getDetail: (id: number) => get(`/wxhall/water-bill/detail/${id}`),
16
+  
17
+  // 申请缴费
18
+  applyPayment: (id: number) => post(`/wxhall/water-bill/payment/${id}`),
19
+  
20
+  // 获取缴费记录
21
+  getPaymentRecords: (params?: {
22
+    page?: number
23
+    size?: number
24
+    startTime?: string
25
+    endTime?: string
26
+    status?: number
27
+  }) => get('/wxhall/water-bill/records', params)
28
+}
29
+
30
+// 报装申请
31
+export const installApplyApi = {
32
+  // 获取报装申请列表
33
+  getList: (params?: {
34
+    page?: number
35
+    size?: number
36
+    status?: number
37
+    type?: string
38
+  }) => get('/wxhall/install-apply/list', params),
39
+  
40
+  // 提交报装申请
41
+  submit: (data: any) => post('/wxhall/install-apply/submit', data),
42
+  
43
+  // 获取申请详情
44
+  getDetail: (id: number) => get(`/wxhall/install-apply/detail/${id}`),
45
+  
46
+  // 取消申请
47
+  cancel: (id: number) => put(`/wxhall/install-apply/cancel/${id}`)
48
+}
49
+
50
+// 公告通知
51
+export const noticeApi = {
52
+  // 获取公告列表
53
+  getList: (params?: {
54
+    page?: number
55
+    size?: number
56
+    type?: string
57
+    status?: number
58
+  }) => get('/wxhall/notice/list', params),
59
+  
60
+  // 获取公告详情
61
+  getDetail: (id: number) => get(`/wxhall/notice/detail/${id}`)
62
+}
63
+
64
+// 用户绑定
65
+export const userBindApi = {
66
+  // 获取绑定记录
67
+  getList: (params?: {
68
+    page?: number
69
+    size?: number
70
+    status?: number
71
+  }) => get('/wxhall/user-bind/list', params),
72
+  
73
+  // 创建绑定关系
74
+  create: (data: any) => post('/wxhall/user-bind/create', data),
75
+  
76
+  // 解除绑定
77
+  unbind: (id: number) => del(`/wxhall/user-bind/delete/${id}`)
78
+}

+ 447
- 2
frontend/src/assets/style.scss Zobrazit soubor

1
-body { margin:0; padding:0; font-family:"Microsoft YaHei","PingFang SC",sans-serif }
2
-#app { height:100vh }
1
+// 全局样式
2
+
3
+// 重置样式
4
+* {
5
+  margin: 0;
6
+  padding: 0;
7
+  box-sizing: border-box;
8
+}
9
+
10
+html, body {
11
+  height: 100%;
12
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
13
+  font-size: 14px;
14
+  line-height: 1.5;
15
+  color: #333;
16
+  background-color: #f5f7fa;
17
+}
18
+
19
+// 滚动条样式
20
+::-webkit-scrollbar {
21
+  width: 8px;
22
+  height: 8px;
23
+}
24
+
25
+::-webkit-scrollbar-track {
26
+  background: #f1f1f1;
27
+  border-radius: 4px;
28
+}
29
+
30
+::-webkit-scrollbar-thumb {
31
+  background: #c1c1c1;
32
+  border-radius: 4px;
33
+}
34
+
35
+::-webkit-scrollbar-thumb:hover {
36
+  background: #a8a8a8;
37
+}
38
+
39
+// Element Plus 样式覆盖
40
+.el-card {
41
+  border: none;
42
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
43
+  
44
+  .el-card__header {
45
+    padding: 16px 20px;
46
+    border-bottom: 1px solid #ebeef5;
47
+    
48
+    .el-card__header-title {
49
+      font-size: 16px;
50
+      font-weight: 500;
51
+    }
52
+  }
53
+}
54
+
55
+.el-button {
56
+  border-radius: 6px;
57
+  
58
+  &--primary {
59
+    background: linear-gradient(135deg, #409EFF 0%, #337ecc 100%);
60
+    border: none;
61
+    
62
+    &:hover {
63
+      background: linear-gradient(135deg, #66b1ff 0%, #409eff 100%);
64
+    }
65
+  }
66
+}
67
+
68
+.el-table {
69
+  border-radius: 8px;
70
+  overflow: hidden;
71
+  
72
+  .el-table__header-wrapper {
73
+    th {
74
+      background-color: #f8f9fa;
75
+      color: #606266;
76
+      font-weight: 500;
77
+    }
78
+  }
79
+}
80
+
81
+.el-form {
82
+  .el-form-item {
83
+    margin-bottom: 22px;
84
+    
85
+    .el-form-item__label {
86
+      font-weight: 500;
87
+      color: #606266;
88
+    }
89
+  }
90
+}
91
+
92
+.el-dialog {
93
+  border-radius: 8px;
94
+  
95
+  .el-dialog__header {
96
+    padding: 20px 24px;
97
+    border-bottom: 1px solid #ebeef5;
98
+    
99
+    .el-dialog__title {
100
+      font-size: 18px;
101
+      font-weight: 500;
102
+    }
103
+  }
104
+  
105
+  .el-dialog__body {
106
+    padding: 24px;
107
+  }
108
+  
109
+  .el-dialog__footer {
110
+    padding: 16px 24px;
111
+    border-top: 1px solid #ebeef5;
112
+  }
113
+}
114
+
115
+.el-select {
116
+  .el-input__inner {
117
+    border-radius: 6px;
118
+  }
119
+}
120
+
121
+.el-input {
122
+  .el-input__inner {
123
+    border-radius: 6px;
124
+  }
125
+}
126
+
127
+.el-textarea {
128
+  .el-textarea__inner {
129
+    border-radius: 6px;
130
+    resize: vertical;
131
+  }
132
+}
133
+
134
+.el-radio {
135
+  margin-right: 20px;
136
+  
137
+  &:last-child {
138
+    margin-right: 0;
139
+  }
140
+}
141
+
142
+.el-checkbox {
143
+  margin-right: 20px;
144
+  
145
+  &:last-child {
146
+    margin-right: 0;
147
+  }
148
+}
149
+
150
+// 布局类
151
+.flex-center {
152
+  display: flex;
153
+  align-items: center;
154
+  justify-content: center;
155
+}
156
+
157
+.flex-between {
158
+  display: flex;
159
+  align-items: center;
160
+  justify-content: space-between;
161
+}
162
+
163
+.flex-column {
164
+  display: flex;
165
+  flex-direction: column;
166
+}
167
+
168
+.flex-wrap {
169
+  display: flex;
170
+  flex-wrap: wrap;
171
+}
172
+
173
+// 间距类
174
+.m-10 { margin: 10px; }
175
+.mt-10 { margin-top: 10px; }
176
+.mr-10 { margin-right: 10px; }
177
+.mb-10 { margin-bottom: 10px; }
178
+.ml-10 { margin-left: 10px; }
179
+
180
+.p-10 { padding: 10px; }
181
+.pt-10 { padding-top: 10px; }
182
+.pr-10 { padding-right: 10px; }
183
+.pb-10 { padding-bottom: 10px; }
184
+.pl-10 { padding-left: 10px; }
185
+
186
+// 颜色类
187
+.text-primary { color: #409EFF; }
188
+.text-success { color: #67C23A; }
189
+.text-warning { color: #E6A23C; }
190
+.text-danger { color: #F56C6C; }
191
+.text-info { color: #909399; }
192
+
193
+.bg-primary { background-color: #409EFF; }
194
+.bg-success { background-color: #67C23A; }
195
+.bg-warning { background-color: #E6A23C; }
196
+.bg-danger { background-color: #F56C6C; }
197
+.bg-info { background-color: #909399; }
198
+
199
+// 边框类
200
+.border {
201
+  border: 1px solid #DCDFE6;
202
+}
203
+
204
+.border-primary {
205
+  border-color: #409EFF;
206
+}
207
+
208
+.border-success {
209
+  border-color: #67C23A;
210
+}
211
+
212
+.border-warning {
213
+  border-color: #E6A23C;
214
+}
215
+
216
+.border-danger {
217
+  border-color: #F56C6C;
218
+}
219
+
220
+.border-info {
221
+  border-color: #909399;
222
+}
223
+
224
+// 圆角类
225
+.rounded {
226
+  border-radius: 4px;
227
+}
228
+
229
+.rounded-md {
230
+  border-radius: 6px;
231
+}
232
+
233
+.rounded-lg {
234
+  border-radius: 8px;
235
+}
236
+
237
+.rounded-full {
238
+  border-radius: 9999px;
239
+}
240
+
241
+// 阴影类
242
+.shadow {
243
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
244
+}
245
+
246
+.shadow-md {
247
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
248
+}
249
+
250
+.shadow-lg {
251
+  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
252
+}
253
+
254
+// 文字样式
255
+.text-sm { font-size: 12px; }
256
+.text-base { font-size: 14px; }
257
+.text-lg { font-size: 16px; }
258
+.text-xl { font-size: 18px; }
259
+.text-2xl { font-size: 20px; }
260
+
261
+.font-bold { font-weight: bold; }
262
+.font-normal { font-weight: normal; }
263
+
264
+.text-center { text-align: center; }
265
+.text-left { text-align: left; }
266
+.text-right { text-align: right; }
267
+
268
+// 页面容器
269
+.page-container {
270
+  padding: 20px;
271
+  background-color: #f5f7fa;
272
+  min-height: calc(100vh - 60px);
273
+}
274
+
275
+// 卡片样式
276
+.page-card {
277
+  background: #fff;
278
+  border-radius: 8px;
279
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
280
+  margin-bottom: 20px;
281
+  
282
+  .page-card-header {
283
+    padding: 20px;
284
+    border-bottom: 1px solid #ebeef5;
285
+    
286
+    h3 {
287
+      margin: 0;
288
+      font-size: 16px;
289
+      font-weight: 500;
290
+      color: #303133;
291
+    }
292
+  }
293
+  
294
+  .page-card-body {
295
+    padding: 20px;
296
+  }
297
+}
298
+
299
+// 表单样式
300
+.form-container {
301
+  max-width: 600px;
302
+  margin: 0 auto;
303
+}
304
+
305
+// 按钮组
306
+.button-group {
307
+  display: flex;
308
+  gap: 10px;
309
+  
310
+  .el-button {
311
+    flex: 1;
312
+  }
313
+}
314
+
315
+// 状态标签
316
+.status-tag {
317
+  padding: 4px 8px;
318
+  border-radius: 12px;
319
+  font-size: 12px;
320
+  font-weight: 500;
321
+  
322
+  &.status-success {
323
+    background-color: #f0f9ff;
324
+    color: #67c23a;
325
+    border: 1px solid #67c23a;
326
+  }
327
+  
328
+  &.status-warning {
329
+    background-color: #fdf6ec;
330
+    color: #e6a23c;
331
+    border: 1px solid #e6a23c;
332
+  }
333
+  
334
+  &.status-danger {
335
+    background-color: #fef0f0;
336
+    color: #f56c6c;
337
+    border: 1px solid #f56c6c;
338
+  }
339
+  
340
+  &.status-info {
341
+    background-color: #f4f4f5;
342
+    color: #909399;
343
+    border: 1px solid #909399;
344
+  }
345
+}
346
+
347
+// 加载动画
348
+.loading-container {
349
+  display: flex;
350
+  align-items: center;
351
+  justify-content: center;
352
+  min-height: 200px;
353
+  
354
+  .el-loading-spinner {
355
+    .circular {
356
+      width: 40px;
357
+      height: 40px;
358
+    }
359
+  }
360
+}
361
+
362
+// 空状态
363
+.empty-state {
364
+  display: flex;
365
+  flex-direction: column;
366
+  align-items: center;
367
+  justify-content: center;
368
+  padding: 60px 20px;
369
+  color: #909399;
370
+  
371
+  .empty-icon {
372
+    font-size: 64px;
373
+    margin-bottom: 16px;
374
+    opacity: 0.5;
375
+  }
376
+  
377
+  .empty-text {
378
+    font-size: 16px;
379
+    margin-bottom: 16px;
380
+  }
381
+  
382
+  .empty-description {
383
+    font-size: 14px;
384
+    color: #c0c4cc;
385
+    margin-bottom: 24px;
386
+  }
387
+}
388
+
389
+// 响应式样式
390
+@media (max-width: 768px) {
391
+  .page-container {
392
+    padding: 10px;
393
+  }
394
+  
395
+  .el-form-item {
396
+    margin-bottom: 16px;
397
+  }
398
+  
399
+  .el-table {
400
+    font-size: 12px;
401
+  }
402
+  
403
+  .el-button {
404
+    padding: 8px 16px;
405
+    font-size: 14px;
406
+  }
407
+}
408
+
409
+// 暗色主题
410
+html.dark {
411
+  color: #e5eaf3;
412
+  background-color: #141414;
413
+  
414
+  .el-card {
415
+    background-color: #1f1f1f;
416
+    border-color: #303030;
417
+    
418
+    .el-card__header {
419
+      border-bottom-color: #303030;
420
+      background-color: #1f1f1f;
421
+    }
422
+  }
423
+  
424
+  .el-table {
425
+    background-color: #1f1f1f;
426
+    color: #e5eaf3;
427
+    
428
+    .el-table__header-wrapper {
429
+      th {
430
+        background-color: #2a2a2a;
431
+        color: #e5eaf3;
432
+      }
433
+    }
434
+  }
435
+  
436
+  .el-dialog {
437
+    background-color: #1f1f1f;
438
+    
439
+    .el-dialog__header {
440
+      border-bottom-color: #303030;
441
+    }
442
+    
443
+    .el-dialog__footer {
444
+      border-top-color: #303030;
445
+    }
446
+  }
447
+}

+ 148
- 0
frontend/src/components/charts/BaseChart.vue Zobrazit soubor

1
+<template>
2
+  <div
3
+    ref="chartRef"
4
+    :style="{
5
+      width: width + 'px',
6
+      height: height + 'px'
7
+    }"
8
+    class="base-chart"
9
+  />
10
+</template>
11
+
12
+<script setup lang="ts">
13
+import { ref, onMounted, onUnmounted, watch } from 'vue'
14
+import * as echarts from 'echarts'
15
+
16
+const props = defineProps({
17
+  // 图表配置项
18
+  option: {
19
+    type: Object,
20
+    required: true
21
+  },
22
+  // 图表宽度
23
+  width: {
24
+    type: Number,
25
+    default: 400
26
+  },
27
+  // 图表高度
28
+  height: {
29
+    type: Number,
30
+    default: 300
31
+  },
32
+  // 是否自动调整大小
33
+  autoResize: {
34
+    type: Boolean,
35
+    default: true
36
+  },
37
+  // 图表主题
38
+  theme: {
39
+    type: String,
40
+    default: ''
41
+  }
42
+})
43
+
44
+const emit = defineEmits(['chartReady', 'chartClick', 'chartMouseDown', 'chartMouseUp'])
45
+
46
+const chartRef = ref<HTMLElement>()
47
+let chartInstance: echarts.ECharts | null = null
48
+
49
+// 初始化图表
50
+const initChart = () => {
51
+  if (!chartRef.value) return
52
+  
53
+  // 销毁旧实例
54
+  if (chartInstance) {
55
+    chartInstance.dispose()
56
+  }
57
+  
58
+  // 创建新实例
59
+  chartInstance = echarts.init(chartRef.value, props.theme)
60
+  
61
+  // 设置配置项
62
+  chartInstance.setOption(props.option)
63
+  
64
+  // 绑定事件
65
+  chartInstance.on('click', (params) => {
66
+    emit('chartClick', params)
67
+  })
68
+  
69
+  chartInstance.on('mousedown', (params) => {
70
+    emit('chartMouseDown', params)
71
+  })
72
+  
73
+  chartInstance.on('mouseup', (params) => {
74
+    emit('chartMouseUp', params)
75
+  })
76
+  
77
+  emit('chartReady', chartInstance)
78
+}
79
+
80
+// 调整图表大小
81
+const resizeChart = () => {
82
+  if (chartInstance) {
83
+    chartInstance.resize()
84
+  }
85
+}
86
+
87
+// 监听配置项变化
88
+watch(() => props.option, (newOption) => {
89
+  if (chartInstance) {
90
+    chartInstance.setOption(newOption)
91
+  }
92
+}, { deep: true })
93
+
94
+// 监听主题变化
95
+watch(() => props.theme, (newTheme) => {
96
+  if (chartInstance && chartRef.value) {
97
+    chartInstance.dispose()
98
+    chartInstance = echarts.init(chartRef.value, newTheme)
99
+    chartInstance.setOption(props.option)
100
+  }
101
+})
102
+
103
+// 监听窗口大小变化
104
+const handleResize = () => {
105
+  if (props.autoResize) {
106
+    resizeChart()
107
+  }
108
+}
109
+
110
+onMounted(() => {
111
+  initChart()
112
+  
113
+  if (props.autoResize) {
114
+    window.addEventListener('resize', handleResize)
115
+  }
116
+})
117
+
118
+onUnmounted(() => {
119
+  if (chartInstance) {
120
+    chartInstance.dispose()
121
+    chartInstance = null
122
+  }
123
+  
124
+  if (props.autoResize) {
125
+    window.removeEventListener('resize', handleResize)
126
+  }
127
+})
128
+
129
+// 暴露方法给父组件
130
+defineExpose({
131
+  getInstance: () => chartInstance,
132
+  resize: resizeChart,
133
+  getOption: () => props.option,
134
+  setOption: (option: any) => {
135
+    if (chartInstance) {
136
+      chartInstance.setOption(option)
137
+    }
138
+  }
139
+})
140
+</script>
141
+
142
+<style scoped lang="scss">
143
+.base-chart {
144
+  display: flex;
145
+  align-items: center;
146
+  justify-content: center;
147
+}
148
+</style>

+ 143
- 0
frontend/src/components/charts/ECharts.vue Zobrazit soubor

1
+<template>
2
+  <div
3
+    ref="chartRef"
4
+    :style="{
5
+      width: width + 'px',
6
+      height: height + 'px'
7
+    }"
8
+    class="echarts"
9
+  />
10
+</template>
11
+
12
+<script setup lang="ts">
13
+import { ref, onMounted, onUnmounted, watch } from 'vue'
14
+import * as echarts from 'echarts'
15
+
16
+const props = defineProps({
17
+  option: {
18
+    type: Object,
19
+    required: true
20
+  },
21
+  width: {
22
+    type: Number,
23
+    default: 400
24
+  },
25
+  height: {
26
+    type: Number,
27
+    default: 300
28
+  },
29
+  autoResize: {
30
+    type: Boolean,
31
+    default: true
32
+  },
33
+  theme: {
34
+    type: String,
35
+    default: ''
36
+  }
37
+})
38
+
39
+const emit = defineEmits(['chartReady', 'chartClick', 'chartMouseDown', 'chartMouseUp'])
40
+
41
+const chartRef = ref<HTMLElement>()
42
+let chartInstance: echarts.ECharts | null = null
43
+
44
+// 初始化图表
45
+const initChart = () => {
46
+  if (!chartRef.value) return
47
+  
48
+  // 销毁旧实例
49
+  if (chartInstance) {
50
+    chartInstance.dispose()
51
+  }
52
+  
53
+  // 创建新实例
54
+  chartInstance = echarts.init(chartRef.value, props.theme)
55
+  
56
+  // 设置配置项
57
+  chartInstance.setOption(props.option)
58
+  
59
+  // 绑定事件
60
+  chartInstance.on('click', (params) => {
61
+    emit('chartClick', params)
62
+  })
63
+  
64
+  chartInstance.on('mousedown', (params) => {
65
+    emit('chartMouseDown', params)
66
+  })
67
+  
68
+  chartInstance.on('mouseup', (params) => {
69
+    emit('chartMouseUp', params)
70
+  })
71
+  
72
+  emit('chartReady', chartInstance)
73
+}
74
+
75
+// 调整图表大小
76
+const resizeChart = () => {
77
+  if (chartInstance) {
78
+    chartInstance.resize()
79
+  }
80
+}
81
+
82
+// 监听配置项变化
83
+watch(() => props.option, (newOption) => {
84
+  if (chartInstance) {
85
+    chartInstance.setOption(newOption)
86
+  }
87
+}, { deep: true })
88
+
89
+// 监听主题变化
90
+watch(() => props.theme, (newTheme) => {
91
+  if (chartInstance && chartRef.value) {
92
+    chartInstance.dispose()
93
+    chartInstance = echarts.init(chartRef.value, newTheme)
94
+    chartInstance.setOption(props.option)
95
+  }
96
+})
97
+
98
+// 监听窗口大小变化
99
+const handleResize = () => {
100
+  if (props.autoResize) {
101
+    resizeChart()
102
+  }
103
+}
104
+
105
+onMounted(() => {
106
+  initChart()
107
+  
108
+  if (props.autoResize) {
109
+    window.addEventListener('resize', handleResize)
110
+  }
111
+})
112
+
113
+onUnmounted(() => {
114
+  if (chartInstance) {
115
+    chartInstance.dispose()
116
+    chartInstance = null
117
+  }
118
+  
119
+  if (props.autoResize) {
120
+    window.removeEventListener('resize', handleResize)
121
+  }
122
+})
123
+
124
+// 暴露方法给父组件
125
+defineExpose({
126
+  getInstance: () => chartInstance,
127
+  resize: resizeChart,
128
+  getOption: () => props.option,
129
+  setOption: (option: any) => {
130
+    if (chartInstance) {
131
+      chartInstance.setOption(option)
132
+    }
133
+  }
134
+})
135
+</script>
136
+
137
+<style scoped lang="scss">
138
+.echarts {
139
+  display: flex;
140
+  align-items: center;
141
+  justify-content: center;
142
+}
143
+</style>

+ 230
- 32
frontend/src/components/layout/MainLayout.vue Zobrazit soubor

1
 <template>
1
 <template>
2
-  <el-container style="height:100vh">
3
-    <el-aside width="220px" class="sidebar">
4
-      <div class="logo">💧 智慧水务</div>
5
-      <el-menu :default-active="route.path" router background-color="#304156" text-color="#bfcbd9" active-text-color="#409EFF">
6
-        <el-menu-item index="/dashboard"><el-icon><DataAnalysis /></el-icon>供水总览</el-menu-item>
7
-        <el-sub-menu index="system">
8
-          <template #title><el-icon><Setting /></el-icon>系统管理</template>
2
+  <el-container class="layout-container">
3
+    <el-aside width="200px" class="sidebar">
4
+      <div class="logo">
5
+        <img src="/logo.png" alt="Logo" class="logo-img" />
6
+        <span class="logo-text">供水管理系统</span>
7
+      </div>
8
+      <el-menu
9
+        :default-active="$route.path"
10
+        class="el-menu-vertical"
11
+        :collapse="isCollapse"
12
+        background-color="#304156"
13
+        text-color="#fff"
14
+        active-text-color="#409EFF"
15
+        router
16
+      >
17
+        <el-menu-item index="/dashboard">
18
+          <el-icon><HomeFilled /></el-icon>
19
+          <template #title>工作台</template>
20
+        </el-menu-item>
21
+        
22
+        <el-sub-menu index="/operation">
23
+          <template #title>
24
+            <el-icon><Operation /></el-icon>
25
+            <span>运营管理</span>
26
+          </template>
27
+          <el-menu-item index="/operation/dashboard">运营总览</el-menu-item>
28
+        </el-sub-menu>
29
+
30
+        <el-sub-menu index="/system">
31
+          <template #title>
32
+            <el-icon><Setting /></el-icon>
33
+            <span>系统管理</span>
34
+          </template>
9
           <el-menu-item index="/system/user">用户管理</el-menu-item>
35
           <el-menu-item index="/system/user">用户管理</el-menu-item>
10
           <el-menu-item index="/system/role">角色管理</el-menu-item>
36
           <el-menu-item index="/system/role">角色管理</el-menu-item>
11
           <el-menu-item index="/system/menu">菜单管理</el-menu-item>
37
           <el-menu-item index="/system/menu">菜单管理</el-menu-item>
12
           <el-menu-item index="/system/dept">部门管理</el-menu-item>
38
           <el-menu-item index="/system/dept">部门管理</el-menu-item>
13
         </el-sub-menu>
39
         </el-sub-menu>
40
+
41
+        <el-sub-menu index="/dispatch">
42
+          <template #title>
43
+            <el-icon><Menu /></el-icon>
44
+            <span>调度管理</span>
45
+          </template>
46
+          <el-menu-item index="/dispatch-command">调度命令</el-menu-item>
47
+        </el-sub-menu>
48
+
49
+        <el-sub-menu index="/service">
50
+          <template #title>
51
+            <el-icon><Service /></el-icon>
52
+            <span>服务中心</span>
53
+          </template>
54
+          <el-menu-item index="/service/workbench">客服工作台</el-menu-item>
55
+        </el-sub-menu>
56
+
57
+        <el-sub-menu index="/wx-hall">
58
+          <template #title>
59
+            <el-icon><Shop /></el-icon>
60
+            <span>微信网厅</span>
61
+          </template>
62
+          <el-menu-item index="/wx-hall/water-bill">水费查询</el-menu-item>
63
+          <el-menu-item index="/wx-hall/install-apply">报装申请</el-menu-item>
64
+          <el-menu-item index="/wx-hall/notice">公告通知</el-menu-item>
65
+          <el-menu-item index="/wx-hall/user-bind">用户绑定</el-menu-item>
66
+        </el-sub-menu>
14
       </el-menu>
67
       </el-menu>
15
     </el-aside>
68
     </el-aside>
69
+
16
     <el-container>
70
     <el-container>
17
-      <el-header class="topbar">
18
-        <span class="title">精河县供水工程综合管理平台</span>
19
-        <el-dropdown @command="handleCommand">
20
-          <span class="user">{{ userStore.realName || '管理员' }} <el-icon><ArrowDown /></el-icon></span>
21
-          <template #dropdown>
22
-            <el-dropdown-menu>
23
-              <el-dropdown-item>个人中心</el-dropdown-item>
24
-              <el-dropdown-item command="logout">退出登录</el-dropdown-item>
25
-            </el-dropdown-menu>
26
-          </template>
27
-        </el-dropdown>
71
+      <el-header class="header">
72
+        <div class="header-left">
73
+          <el-button type="text" @click="toggleSidebar">
74
+            <el-icon><Fold v-if="!isCollapse" /><Expand v-else /></el-icon>
75
+          </el-button>
76
+          <breadcrumb />
77
+        </div>
78
+        <div class="header-right">
79
+          <el-button type="text" @click="toggleFullscreen">
80
+            <el-icon><FullScreen /></el-icon>
81
+          </el-button>
82
+          <el-dropdown trigger="click">
83
+            <div class="user-info">
84
+              <el-avatar :size="32" src="/avatar.png" />
85
+              <span class="username">{{ username }}</span>
86
+            </div>
87
+            <template #dropdown>
88
+              <el-dropdown-menu>
89
+                <el-dropdown-item @click="goToProfile">个人中心</el-dropdown-item>
90
+                <el-dropdown-item divided @click="handleLogout">退出登录</el-dropdown-item>
91
+              </el-dropdown-menu>
92
+            </template>
93
+          </el-dropdown>
94
+        </div>
28
       </el-header>
95
       </el-header>
29
-      <el-main><router-view /></el-main>
96
+
97
+      <el-main class="main-content">
98
+        <router-view />
99
+      </el-main>
30
     </el-container>
100
     </el-container>
31
   </el-container>
101
   </el-container>
32
 </template>
102
 </template>
33
 
103
 
34
 <script setup lang="ts">
104
 <script setup lang="ts">
35
-import { useRoute, useRouter } from 'vue-router'
36
-import { useUserStore } from '@/store/user'
105
+import { ref, onMounted } from 'vue'
106
+import { useRouter, useRoute } from 'vue-router'
107
+import { ElMessage } from 'element-plus'
108
+import {
109
+  HomeFilled,
110
+  Operation,
111
+  Setting,
112
+  Menu,
113
+  Service,
114
+  Shop,
115
+  Fold,
116
+  Expand,
117
+  FullScreen
118
+} from '@element-plus/icons-vue'
37
 
119
 
38
-const route = useRoute()
39
 const router = useRouter()
120
 const router = useRouter()
40
-const userStore = useUserStore()
121
+const route = useRoute()
122
+
123
+const isCollapse = ref(false)
124
+const username = ref('admin')
125
+
126
+const toggleSidebar = () => {
127
+  isCollapse.value = !isCollapse.value
128
+}
129
+
130
+const toggleFullscreen = () => {
131
+  if (!document.fullscreenElement) {
132
+    document.documentElement.requestFullscreen()
133
+  } else {
134
+    if (document.exitFullscreen) {
135
+      document.exitFullscreen()
136
+    }
137
+  }
138
+}
139
+
140
+const goToProfile = () => {
141
+  router.push('/profile')
142
+}
41
 
143
 
42
-function handleCommand(cmd: string) {
43
-  if (cmd === 'logout') { userStore.logout(); router.push('/login') }
144
+const handleLogout = () => {
145
+  localStorage.removeItem('token')
146
+  ElMessage.success('退出成功')
147
+  router.push('/login')
44
 }
148
 }
149
+
150
+// 面包屑组件
151
+const Breadcrumb = () => {
152
+  return (
153
+    <el-breadcrumb separator="/">
154
+      <el-breadcrumb-item>首页</el-breadcrumb-item>
155
+      <el-breadcrumb-item>{{ route.meta?.title || route.name }}</el-breadcrumb-item>
156
+    </el-breadcrumb>
157
+  )
158
+}
159
+
160
+onMounted(() => {
161
+  // 从 localStorage 获取用户信息
162
+  const userInfo = localStorage.getItem('userInfo')
163
+  if (userInfo) {
164
+    try {
165
+      const user = JSON.parse(userInfo)
166
+      username.value = user.username || 'admin'
167
+    } catch (e) {
168
+      console.error('解析用户信息失败:', e)
169
+    }
170
+  }
171
+})
45
 </script>
172
 </script>
46
 
173
 
47
-<style scoped>
48
-.sidebar { background:#304156; overflow-y:auto }
49
-.logo { color:#fff; text-align:center; padding:16px; font-size:18px; font-weight:bold; border-bottom:1px solid rgba(255,255,255,.1) }
50
-.topbar { background:#fff; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid #e6e6e6 }
51
-.title { font-size:16px; color:#333 }
52
-.user { cursor:pointer; color:#666 }
53
-</style>
174
+<style scoped lang="scss">
175
+.layout-container {
176
+  height: 100vh;
177
+  
178
+  .sidebar {
179
+    background-color: #304156;
180
+    transition: width 0.3s;
181
+    
182
+    .logo {
183
+      height: 60px;
184
+      display: flex;
185
+      align-items: center;
186
+      padding: 0 20px;
187
+      background-color: #263445;
188
+      
189
+      .logo-img {
190
+        width: 32px;
191
+        height: 32px;
192
+        margin-right: 10px;
193
+      }
194
+      
195
+      .logo-text {
196
+        color: #fff;
197
+        font-size: 16px;
198
+        font-weight: 600;
199
+      }
200
+    }
201
+    
202
+    .el-menu-vertical {
203
+      border-right: none;
204
+    }
205
+  }
206
+  
207
+  .header {
208
+    background-color: #fff;
209
+    border-bottom: 1px solid #e6e6e6;
210
+    display: flex;
211
+    align-items: center;
212
+    justify-content: space-between;
213
+    padding: 0 20px;
214
+    
215
+    .header-left {
216
+      display: flex;
217
+      align-items: center;
218
+      
219
+      .el-button {
220
+        margin-right: 20px;
221
+      }
222
+    }
223
+    
224
+    .header-right {
225
+      display: flex;
226
+      align-items: center;
227
+      
228
+      .el-button {
229
+        margin: 0 10px;
230
+      }
231
+      
232
+      .user-info {
233
+        display: flex;
234
+        align-items: center;
235
+        cursor: pointer;
236
+        
237
+        .username {
238
+          margin-left: 8px;
239
+          font-size: 14px;
240
+        }
241
+      }
242
+    }
243
+  }
244
+  
245
+  .main-content {
246
+    background-color: #f5f7fa;
247
+    padding: 20px;
248
+    overflow-y: auto;
249
+  }
250
+}
251
+</style>

+ 85
- 16
frontend/src/router/index.ts Zobrazit soubor

1
 import { createRouter, createWebHistory } from 'vue-router'
1
 import { createRouter, createWebHistory } from 'vue-router'
2
 
2
 
3
 const routes = [
3
 const routes = [
4
-  { path: '/login', name: 'login', component: () => import('@/views/login/LoginView.vue') },
4
+  { path: '/login', name: 'login', component: () => import('@/views/login/LoginView.vue'), meta: { title: '登录' } },
5
+  { path: '/register', name: 'register', component: () => import('@/views/login/RegisterView.vue'), meta: { title: '注册' } },
6
+  { path: '/forgot-password', name: 'forgotPassword', component: () => import('@/views/login/ForgotPasswordView.vue'), meta: { title: '忘记密码' } },
5
   {
7
   {
6
-    path: '/', component: () => import('@/components/layout/MainLayout.vue'),
8
+    path: '/', 
9
+    component: () => import('@/components/layout/MainLayout.vue'),
7
     redirect: '/dashboard',
10
     redirect: '/dashboard',
8
     children: [
11
     children: [
9
-{ path: 'operation', name: 'operation', component: () => import('@/views/dashboard/OperationDashboard.vue') },
10
-      { path: 'dashboard', name: 'dashboard', component: () => import('@/views/dashboard/DashboardView.vue') },
11
-      { path: 'system/user', name: 'user', component: () => import('@/views/system/user/UserList.vue') },
12
-      { path: 'system/role', name: 'role', component: () => import('@/views/system/role/RoleList.vue') },
13
-      { path: 'system/menu', name: 'menu', component: () => import('@/views/system/menu/MenuList.vue') },
14
-      { path: 'system/dept', name: 'dept', component: () => import('@/views/system/dept/DeptList.vue') },
15
-      { path: 'dispatch-command', name: 'dispatchCommandList', component: () => import('@/views/dispatch-command/CommandList.vue') },
16
-      { path: 'dispatch-command/:id', name: 'dispatchCommandDetail', component: () => import('@/views/dispatch-command/CommandDetail.vue') },
17
-      { path: 'service/workbench', name: 'serviceWorkbench', component: () => import('@/views/service/CustomerServiceWorkbench.vue') },
12
+      { 
13
+        path: 'dashboard', 
14
+        name: 'dashboard', 
15
+        component: () => import('@/views/dashboard/DashboardView.vue'),
16
+        meta: { title: '工作台' }
17
+      },
18
+      { 
19
+        path: 'operation', 
20
+        name: 'operation', 
21
+        component: () => import('@/views/dashboard/OperationDashboard.vue'),
22
+        meta: { title: '运营管理' }
23
+      },
24
+      { 
25
+        path: 'system/user', 
26
+        name: 'user', 
27
+        component: () => import('@/views/system/user/UserList.vue'),
28
+        meta: { title: '用户管理' }
29
+      },
30
+      { 
31
+        path: 'system/role', 
32
+        name: 'role', 
33
+        component: () => import('@/views/system/role/RoleList.vue'),
34
+        meta: { title: '角色管理' }
35
+      },
36
+      { 
37
+        path: 'system/menu', 
38
+        name: 'menu', 
39
+        component: () => import('@/views/system/menu/菜单List.vue'),
40
+        meta: { title: '菜单管理' }
41
+      },
42
+      { 
43
+        path: 'system/dept', 
44
+        name: 'dept', 
45
+        component: () => import('@/views/system/dept/DeptList.vue'),
46
+        meta: { title: '部门管理' }
47
+      },
48
+      { 
49
+        path: 'dispatch-command', 
50
+        name: 'dispatchCommandList', 
51
+        component: () => import('@/views/dispatch-command/CommandList.vue'),
52
+        meta: { title: '调度命令' }
53
+      },
54
+      { 
55
+        path: 'dispatch-command/:id', 
56
+        name: 'dispatchCommandDetail', 
57
+        component: () => import('@/views/dispatch-command/CommandDetail.vue'),
58
+        meta: { title: '调度详情' }
59
+      },
60
+      { 
61
+        path: 'service/workbench', 
62
+        name: 'serviceWorkbench', 
63
+        component: () => import('@/views/service/CustomerServiceWorkbench.vue'),
64
+        meta: { title: '客服工作台' }
65
+      },
18
       // 微信网厅
66
       // 微信网厅
19
-      { path: 'wx-hall/water-bill', name: 'wxWaterBill', component: () => import('@/views/wxhall/WaterBillView.vue') },
20
-      { path: 'wx-hall/install-apply', name: 'wxInstallApply', component: () => import('@/views/wxhall/InstallApplyView.vue') },
21
-      { path: 'wx-hall/notice', name: 'wxNotice', component: () => import('@/views/wxhall/NoticeView.vue') },
22
-      { path: 'wx-hall/user-bind', name: 'wxUserBind', component: () => import('@/views/wxhall/UserBindView.vue') },
67
+      { 
68
+        path: 'wx-hall/water-bill', 
69
+        name: 'wxWaterBill', 
70
+        component: () => import('@/views/wxhall/WaterBillView.vue'),
71
+        meta: { title: '水费查询' }
72
+      },
73
+      { 
74
+        path: 'wx-hall/install-apply', 
75
+        name: 'wxInstallApply', 
76
+        component: () => import('@/views/wxhall/InstallApplyView.vue'),
77
+        meta: { title: '报装申请' }
78
+      },
79
+      { 
80
+        path: 'wx-hall/notice', 
81
+        name: 'wxNotice', 
82
+        component: () => import('@/views/wxhall/NoticeView.vue'),
83
+        meta: { title: '公告通知' }
84
+      },
85
+      { 
86
+        path: 'wx-hall/user-bind', 
87
+        name: 'wxUserBind', 
88
+        component: () => import('@/views/wxhall/UserBindView.vue'),
89
+        meta: { title: '用户绑定' }
90
+      },
23
     ]
91
     ]
24
   },
92
   },
93
+  { path: '/profile', name: 'profile', component: () => import('@/views/profile/ProfileView.vue'), meta: { title: '个人中心' } },
25
   { path: '/:pathMatch(.*)*', redirect: '/dashboard' }
94
   { path: '/:pathMatch(.*)*', redirect: '/dashboard' }
26
 ]
95
 ]
27
 
96
 
33
   else { next() }
102
   else { next() }
34
 })
103
 })
35
 
104
 
36
-export default router
105
+export default router

+ 147
- 0
frontend/src/utils/request.ts Zobrazit soubor

1
+import axios from 'axios'
2
+import { ElMessage } from 'element-plus'
3
+import router from '@/router'
4
+
5
+// 创建 axios 实例
6
+const service = axios.create({
7
+  baseURL: '/api', // API 基础路径
8
+  timeout: 10000,  // 请求超时时间
9
+  headers: {
10
+    'Content-Type': 'application/json'
11
+  }
12
+})
13
+
14
+// 请求拦截器
15
+service.interceptors.request.use(
16
+  (config) => {
17
+    // 从 localStorage 获取 token
18
+    const token = localStorage.getItem('token')
19
+    if (token) {
20
+      config.headers['Authorization'] = `Bearer ${token}`
21
+    }
22
+    return config
23
+  },
24
+  (error) => {
25
+    console.error('请求错误:', error)
26
+    return Promise.reject(error)
27
+  }
28
+)
29
+
30
+// 响应拦截器
31
+service.interceptors.response.use(
32
+  (response) => {
33
+    const { data } = response
34
+    
35
+    // 假设 API 返回格式为 { code: 200, data: {}, message: 'success' }
36
+    if (data.code === 200) {
37
+      return data.data
38
+    } else {
39
+      ElMessage.error(data.message || '请求失败')
40
+      return Promise.reject(new Error(data.message || '请求失败'))
41
+    }
42
+  },
43
+  (error) => {
44
+    let message = '请求失败'
45
+    
46
+    if (error.response) {
47
+      const { status, data } = error.response
48
+      
49
+      switch (status) {
50
+        case 401:
51
+          message = '登录已过期,请重新登录'
52
+          localStorage.removeItem('token')
53
+          localStorage.removeItem('userInfo')
54
+          router.push('/login')
55
+          break
56
+        case 403:
57
+          message = '没有权限访问'
58
+          break
59
+        case 404:
60
+          message = '请求的资源不存在'
61
+          break
62
+        case 500:
63
+          message = '服务器错误'
64
+          break
65
+        default:
66
+          message = data.message || `请求失败 (${status})`
67
+      }
68
+    } else if (error.request) {
69
+      message = '网络错误,请检查网络连接'
70
+    } else {
71
+      message = error.message || '请求失败'
72
+    }
73
+    
74
+    ElMessage.error(message)
75
+    return Promise.reject(error)
76
+  }
77
+)
78
+
79
+// 封装 GET 请求
80
+export const get = (url: string, params?: any) => {
81
+  return service({
82
+    method: 'get',
83
+    url,
84
+    params
85
+  })
86
+}
87
+
88
+// 封装 POST 请求
89
+export const post = (url: string, data?: any) => {
90
+  return service({
91
+    method: 'post',
92
+    url,
93
+    data
94
+  })
95
+}
96
+
97
+// 封装 PUT 请求
98
+export const put = (url: string, data?: any) => {
99
+  return service({
100
+    method: 'put',
101
+    url,
102
+    data
103
+  })
104
+}
105
+
106
+// 封装 DELETE 请求
107
+export const del = (url: string) => {
108
+  return service({
109
+    method: 'delete',
110
+    url
111
+  })
112
+}
113
+
114
+// 封装文件上传
115
+export const upload = (url: string, file: File) => {
116
+  const formData = new FormData()
117
+  formData.append('file', file)
118
+  
119
+  return service({
120
+    method: 'post',
121
+    url,
122
+    data: formData,
123
+    headers: {
124
+      'Content-Type': 'multipart/form-data'
125
+    }
126
+  })
127
+}
128
+
129
+// 封装文件下载
130
+export const download = (url: string, filename?: string) => {
131
+  return service({
132
+    method: 'get',
133
+    url,
134
+    responseType: 'blob'
135
+  }).then((data) => {
136
+    const blob = new Blob([data])
137
+    const link = document.createElement('a')
138
+    link.href = URL.createObjectURL(blob)
139
+    link.download = filename || 'download'
140
+    document.body.appendChild(link)
141
+    link.click()
142
+    document.body.removeChild(link)
143
+    URL.revokeObjectURL(link.href)
144
+  })
145
+}
146
+
147
+export default service

+ 675
- 1345
frontend/src/views/dashboard/WaterSupplySpecialScreen.vue
Diff nebyl zobrazen, protože je příliš veliký
Zobrazit soubor


+ 155
- 0
frontend/src/views/login/ForgotPasswordView.vue Zobrazit soubor

1
+<template>
2
+  <div class="forgot-password-container">
3
+    <div class="forgot-password-box">
4
+      <div class="forgot-password-header">
5
+        <img src="/logo.png" alt="Logo" class="forgot-password-logo" />
6
+        <h2>找回密码</h2>
7
+        <p>请输入您的邮箱,我们将发送重置密码的链接</p>
8
+      </div>
9
+      
10
+      <el-form
11
+        ref="forgotPasswordFormRef"
12
+        :model="forgotPasswordForm"
13
+        :rules="forgotPasswordRules"
14
+        class="forgot-password-form"
15
+        size="large"
16
+      >
17
+        <el-form-item prop="email">
18
+          <el-input
19
+            v-model="forgotPasswordForm.email"
20
+            placeholder="请输入邮箱"
21
+            prefix-icon="Message"
22
+            clearable
23
+          />
24
+        </el-form-item>
25
+        
26
+        <el-form-item>
27
+          <el-button
28
+            type="primary"
29
+            :loading="loading"
30
+            class="forgot-password-button"
31
+            @click="handleForgotPassword"
32
+          >
33
+            {{ loading ? '发送中...' : '发送重置链接' }}
34
+          </el-button>
35
+        </el-form-item>
36
+        
37
+        <div class="forgot-password-links">
38
+          <router-link to="/login" class="link">返回登录</router-link>
39
+          <router-link to="/register" class="link">注册账号</router-link>
40
+        </div>
41
+      </el-form>
42
+    </div>
43
+  </div>
44
+</template>
45
+
46
+<script setup lang="ts">
47
+import { ref, reactive } from 'vue'
48
+import { useRouter } from 'vue-router'
49
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
50
+import { Message } from '@element-plus/icons-vue'
51
+
52
+const router = useRouter()
53
+const forgotPasswordFormRef = ref<FormInstance>()
54
+const loading = ref(false)
55
+
56
+const forgotPasswordForm = reactive({
57
+  email: ''
58
+})
59
+
60
+const forgotPasswordRules: FormRules = {
61
+  email: [
62
+    { required: true, message: '请输入邮箱', trigger: 'blur' },
63
+    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
64
+  ]
65
+}
66
+
67
+const handleForgotPassword = async () => {
68
+  if (!forgotPasswordFormRef.value) return
69
+  
70
+  try {
71
+    const valid = await forgotPasswordFormRef.value.validate()
72
+    if (valid) {
73
+      loading.value = true
74
+      
75
+      // 模拟发送重置密码邮件 API 调用
76
+      setTimeout(() => {
77
+        // 这里应该是实际的重置密码邮件发送 API 调用
78
+        // const response = await sendResetPasswordEmail(forgotPasswordForm)
79
+        
80
+        ElMessage.success('重置密码链接已发送到您的邮箱,请查收')
81
+        router.push('/login')
82
+      }, 1500)
83
+    }
84
+  } catch (error) {
85
+    console.error('验证失败:', error)
86
+  } finally {
87
+    loading.value = false
88
+  }
89
+}
90
+</script>
91
+
92
+<style scoped lang="scss">
93
+.forgot-password-container {
94
+  min-height: 100vh;
95
+  display: flex;
96
+  align-items: center;
97
+  justify-content: center;
98
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
99
+  
100
+  .forgot-password-box {
101
+    width: 450px;
102
+    padding: 40px;
103
+    background: rgba(255, 255, 255, 0.95);
104
+    border-radius: 10px;
105
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
106
+    
107
+    .forgot-password-header {
108
+      text-align: center;
109
+      margin-bottom: 40px;
110
+      
111
+      .forgot-password-logo {
112
+        width: 80px;
113
+        height: 80px;
114
+        margin-bottom: 20px;
115
+      }
116
+      
117
+      h2 {
118
+        color: #333;
119
+        font-size: 24px;
120
+        margin-bottom: 10px;
121
+      }
122
+      
123
+      p {
124
+        color: #666;
125
+        font-size: 14px;
126
+      }
127
+    }
128
+    
129
+    .forgot-password-form {
130
+      .forgot-password-button {
131
+        width: 100%;
132
+        height: 44px;
133
+        font-size: 16px;
134
+        border-radius: 6px;
135
+      }
136
+      
137
+      .forgot-password-links {
138
+        margin-top: 20px;
139
+        text-align: center;
140
+        
141
+        .link {
142
+          color: #666;
143
+          text-decoration: none;
144
+          margin: 0 10px;
145
+          font-size: 14px;
146
+          
147
+          &:hover {
148
+            color: #409EFF;
149
+          }
150
+        }
151
+      }
152
+    }
153
+  }
154
+}
155
+</style>

+ 180
- 27
frontend/src/views/login/LoginView.vue Zobrazit soubor

1
 <template>
1
 <template>
2
   <div class="login-container">
2
   <div class="login-container">
3
     <div class="login-box">
3
     <div class="login-box">
4
-      <h1>智慧水务管理系统</h1>
5
-      <el-form :model="form" :rules="rules" ref="formRef">
4
+      <div class="login-header">
5
+        <img src="/logo.png" alt="Logo" class="login-logo" />
6
+        <h2>供水管理系统</h2>
7
+        <p>欢迎登录智慧供水管理平台</p>
8
+      </div>
9
+      
10
+      <el-form
11
+        ref="loginFormRef"
12
+        :model="loginForm"
13
+        :rules="loginRules"
14
+        class="login-form"
15
+        size="large"
16
+      >
6
         <el-form-item prop="username">
17
         <el-form-item prop="username">
7
-          <el-input v-model="form.username" placeholder="用户名" prefix-icon="User" size="large" />
18
+          <el-input
19
+            v-model="loginForm.username"
20
+            placeholder="请输入用户名"
21
+            prefix-icon="User"
22
+            clearable
23
+          />
8
         </el-form-item>
24
         </el-form-item>
25
+        
9
         <el-form-item prop="password">
26
         <el-form-item prop="password">
10
-          <el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" size="large" @keyup.enter="handleLogin" />
27
+          <el-input
28
+            v-model="loginForm.password"
29
+            type="password"
30
+            placeholder="请输入密码"
31
+            prefix-icon="Lock"
32
+            show-password
33
+            @keyup.enter="handleLogin"
34
+          />
11
         </el-form-item>
35
         </el-form-item>
36
+        
12
         <el-form-item>
37
         <el-form-item>
13
-          <el-button type="primary" size="large" style="width:100%" :loading="loading" @click="handleLogin">登 录</el-button>
38
+          <el-checkbox v-model="loginForm.remember">记住我</el-checkbox>
14
         </el-form-item>
39
         </el-form-item>
40
+        
41
+        <el-form-item>
42
+          <el-button
43
+            type="primary"
44
+            :loading="loading"
45
+            class="login-button"
46
+            @click="handleLogin"
47
+          >
48
+            {{ loading ? '登录中...' : '登录' }}
49
+          </el-button>
50
+        </el-form-item>
51
+        
52
+        <div class="login-links">
53
+          <router-link to="/register" class="link">注册账号</router-link>
54
+          <router-link to="/forgot-password" class="link">忘记密码?</router-link>
55
+        </div>
15
       </el-form>
56
       </el-form>
16
     </div>
57
     </div>
17
   </div>
58
   </div>
20
 <script setup lang="ts">
61
 <script setup lang="ts">
21
 import { ref, reactive } from 'vue'
62
 import { ref, reactive } from 'vue'
22
 import { useRouter } from 'vue-router'
63
 import { useRouter } from 'vue-router'
23
-import { useUserStore } from '@/store/user'
24
-import { login } from '@/api/auth'
25
-import { ElMessage } from 'element-plus'
64
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
65
+import { User, Lock } from '@element-plus/icons-vue'
26
 
66
 
27
 const router = useRouter()
67
 const router = useRouter()
28
-const userStore = useUserStore()
29
-const formRef = ref()
68
+const loginFormRef = ref<FormInstance>()
30
 const loading = ref(false)
69
 const loading = ref(false)
31
-const form = reactive({ username: 'admin', password: 'admin123' })
32
-const rules = { username: [{ required: true, message: '请输入用户名' }], password: [{ required: true, message: '请输入密码' }] }
33
 
70
 
34
-async function handleLogin() {
35
-  await formRef.value.validate()
36
-  loading.value = true
71
+const loginForm = reactive({
72
+  username: 'admin',
73
+  password: 'admin123',
74
+  remember: false
75
+})
76
+
77
+const loginRules: FormRules = {
78
+  username: [
79
+    { required: true, message: '请输入用户名', trigger: 'blur' },
80
+    { min: 3, max: 20, message: '用户名长度为 3-20 个字符', trigger: 'blur' }
81
+  ],
82
+  password: [
83
+    { required: true, message: '请输入密码', trigger: 'blur' },
84
+    { min: 6, max: 20, message: '密码长度为 6-20 个字符', trigger: 'blur' }
85
+  ]
86
+}
87
+
88
+const handleLogin = async () => {
89
+  if (!loginFormRef.value) return
90
+  
37
   try {
91
   try {
38
-    const res = await login(form.username, form.password)
39
-    userStore.setToken(res.data)
40
-    ElMessage.success('登录成功')
41
-    router.push('/dashboard')
42
-  } catch (e) {
43
-    console.error(e)
44
-  } finally { loading.value = false }
92
+    const valid = await loginFormRef.value.validate()
93
+    if (valid) {
94
+      loading.value = true
95
+      
96
+      // 模拟登录 API 调用
97
+      setTimeout(() => {
98
+        // 这里应该是实际的登录 API 调用
99
+        // const response = await login(loginForm)
100
+        
101
+        // 模拟成功登录
102
+        const token = 'mock-token-' + Date.now()
103
+        const userInfo = {
104
+          id: 1,
105
+          username: loginForm.username,
106
+          nickname: '管理员',
107
+          avatar: '/avatar.png',
108
+          roles: ['admin'],
109
+          permissions: ['*']
110
+        }
111
+        
112
+        localStorage.setItem('token', token)
113
+        localStorage.setItem('userInfo', JSON.stringify(userInfo))
114
+        
115
+        if (loginForm.remember) {
116
+          localStorage.setItem('rememberedUsername', loginForm.username)
117
+        } else {
118
+          localStorage.removeItem('rememberedUsername')
119
+        }
120
+        
121
+        ElMessage.success('登录成功')
122
+        router.push('/dashboard')
123
+      }, 1500)
124
+    }
125
+  } catch (error) {
126
+    console.error('登录验证失败:', error)
127
+  } finally {
128
+    loading.value = false
129
+  }
45
 }
130
 }
131
+
132
+// 页面加载时检查是否有记住的用户名
133
+onMounted(() => {
134
+  const rememberedUsername = localStorage.getItem('rememberedUsername')
135
+  if (rememberedUsername) {
136
+    loginForm.username = rememberedUsername
137
+    loginForm.remember = true
138
+  }
139
+})
46
 </script>
140
 </script>
47
 
141
 
48
-<style scoped>
49
-.login-container { display:flex; justify-content:center; align-items:center; height:100vh; background:linear-gradient(135deg,#1e3c72 0%,#2a5298 100%) }
50
-.login-box { width:400px; padding:40px; background:#fff; border-radius:8px; box-shadow:0 2px 12px rgba(0,0,0,.3) }
51
-h1 { text-align:center; margin-bottom:30px; color:#1e3c72 }
52
-</style>
142
+<style scoped lang="scss">
143
+.login-container {
144
+  min-height: 100vh;
145
+  display: flex;
146
+  align-items: center;
147
+  justify-content: center;
148
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
149
+  
150
+  .login-box {
151
+    width: 400px;
152
+    padding: 40px;
153
+    background: rgba(255, 255, 255, 0.95);
154
+    border-radius: 10px;
155
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
156
+    
157
+    .login-header {
158
+      text-align: center;
159
+      margin-bottom: 40px;
160
+      
161
+      .login-logo {
162
+        width: 80px;
163
+        height: 80px;
164
+        margin-bottom: 20px;
165
+      }
166
+      
167
+      h2 {
168
+        color: #333;
169
+        font-size: 24px;
170
+        margin-bottom: 10px;
171
+      }
172
+      
173
+      p {
174
+        color: #666;
175
+        font-size: 14px;
176
+      }
177
+    }
178
+    
179
+    .login-form {
180
+      .login-button {
181
+        width: 100%;
182
+        height: 44px;
183
+        font-size: 16px;
184
+        border-radius: 6px;
185
+      }
186
+      
187
+      .login-links {
188
+        margin-top: 20px;
189
+        text-align: center;
190
+        
191
+        .link {
192
+          color: #666;
193
+          text-decoration: none;
194
+          margin: 0 10px;
195
+          font-size: 14px;
196
+          
197
+          &:hover {
198
+            color: #409EFF;
199
+          }
200
+        }
201
+      }
202
+    }
203
+  }
204
+}
205
+</style>

+ 232
- 0
frontend/src/views/login/RegisterView.vue Zobrazit soubor

1
+<template>
2
+  <div class="register-container">
3
+    <div class="register-box">
4
+      <div class="register-header">
5
+        <img src="/logo.png" alt="Logo" class="register-logo" />
6
+        <h2>用户注册</h2>
7
+        <p>创建您的供水管理系统账号</p>
8
+      </div>
9
+      
10
+      <el-form
11
+        ref="registerFormRef"
12
+        :model="registerForm"
13
+        :rules="registerRules"
14
+        class="register-form"
15
+        size="large"
16
+      >
17
+        <el-form-item prop="username">
18
+          <el-input
19
+            v-model="registerForm.username"
20
+            placeholder="请输入用户名"
21
+            prefix-icon="User"
22
+            clearable
23
+          />
24
+        </el-form-item>
25
+        
26
+        <el-form-item prop="password">
27
+          <el-input
28
+            v-model="registerForm.password"
29
+            type="password"
30
+            placeholder="请输入密码"
31
+            prefix-icon="Lock"
32
+            show-password
33
+          />
34
+        </el-form-item>
35
+        
36
+        <el-form-item prop="confirmPassword">
37
+          <el-input
38
+            v-model="registerForm.confirmPassword"
39
+            type="password"
40
+            placeholder="请确认密码"
41
+            prefix-icon="Lock"
42
+            show-password
43
+          />
44
+        </el-form-item>
45
+        
46
+        <el-form-item prop="email">
47
+          <el-input
48
+            v-model="registerForm.email"
49
+            placeholder="请输入邮箱"
50
+            prefix-icon="Message"
51
+            clearable
52
+          />
53
+        </el-form-item>
54
+        
55
+        <el-form-item prop="nickname">
56
+          <el-input
57
+            v-model="registerForm.nickname"
58
+            placeholder="请输入昵称"
59
+            prefix-icon="UserFilled"
60
+            clearable
61
+          />
62
+        </el-form-item>
63
+        
64
+        <el-form-item>
65
+          <el-checkbox v-model="registerForm.agree">我已阅读并同意《用户协议》和《隐私政策》</el-checkbox>
66
+        </el-form-item>
67
+        
68
+        <el-form-item>
69
+          <el-button
70
+            type="primary"
71
+            :loading="loading"
72
+            class="register-button"
73
+            @click="handleRegister"
74
+          >
75
+            {{ loading ? '注册中...' : '注册' }}
76
+          </el-button>
77
+        </el-form-item>
78
+        
79
+        <div class="register-links">
80
+          <router-link to="/login" class="link">返回登录</router-link>
81
+        </div>
82
+      </el-form>
83
+    </div>
84
+  </div>
85
+</template>
86
+
87
+<script setup lang="ts">
88
+import { ref, reactive } from 'vue'
89
+import { useRouter } from 'vue-router'
90
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
91
+import { User, Lock, Message, UserFilled } from '@element-plus/icons-vue'
92
+
93
+const router = useRouter()
94
+const registerFormRef = ref<FormInstance>()
95
+const loading = ref(false)
96
+
97
+const registerForm = reactive({
98
+  username: '',
99
+  password: '',
100
+  confirmPassword: '',
101
+  email: '',
102
+  nickname: '',
103
+  agree: false
104
+})
105
+
106
+const validateConfirmPassword = (rule: any, value: string, callback: any) => {
107
+  if (value === '') {
108
+    callback(new Error('请再次输入密码'))
109
+  } else if (value !== registerForm.password) {
110
+    callback(new Error('两次输入密码不一致'))
111
+  } else {
112
+    callback()
113
+  }
114
+}
115
+
116
+const registerRules: FormRules = {
117
+  username: [
118
+    { required: true, message: '请输入用户名', trigger: 'blur' },
119
+    { min: 3, max: 20, message: '用户名长度为 3-20 个字符', trigger: 'blur' },
120
+    { pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
121
+  ],
122
+  password: [
123
+    { required: true, message: '请输入密码', trigger: 'blur' },
124
+    { min: 6, max: 20, message: '密码长度为 6-20 个字符', trigger: 'blur' },
125
+    { pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{6,}$/, message: '密码必须包含大小写字母和数字', trigger: 'blur' }
126
+  ],
127
+  confirmPassword: [
128
+    { required: true, message: '请确认密码', trigger: 'blur' },
129
+    { validator: validateConfirmPassword, trigger: 'blur' }
130
+  ],
131
+  email: [
132
+    { required: true, message: '请输入邮箱', trigger: 'blur' },
133
+    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
134
+  ],
135
+  nickname: [
136
+    { required: true, message: '请输入昵称', trigger: 'blur' },
137
+    { min: 2, max: 10, message: '昵称长度为 2-10 个字符', trigger: 'blur' }
138
+  ],
139
+  agree: [
140
+    { required: true, message: '请阅读并同意用户协议', trigger: 'change' }
141
+  ]
142
+}
143
+
144
+const handleRegister = async () => {
145
+  if (!registerFormRef.value) return
146
+  
147
+  try {
148
+    const valid = await registerFormRef.value.validate()
149
+    if (valid) {
150
+      loading.value = true
151
+      
152
+      // 模拟注册 API 调用
153
+      setTimeout(() => {
154
+        // 这里应该是实际的注册 API 调用
155
+        // const response = await register(registerForm)
156
+        
157
+        // 模拟注册成功
158
+        ElMessage.success('注册成功!请登录')
159
+        router.push('/login')
160
+      }, 1500)
161
+    }
162
+  } catch (error) {
163
+    console.error('注册验证失败:', error)
164
+  } finally {
165
+    loading.value = false
166
+  }
167
+}
168
+</script>
169
+
170
+<style scoped lang="scss">
171
+.register-container {
172
+  min-height: 100vh;
173
+  display: flex;
174
+  align-items: center;
175
+  justify-content: center;
176
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
177
+  
178
+  .register-box {
179
+    width: 450px;
180
+    padding: 40px;
181
+    background: rgba(255, 255, 255, 0.95);
182
+    border-radius: 10px;
183
+    box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
184
+    
185
+    .register-header {
186
+      text-align: center;
187
+      margin-bottom: 40px;
188
+      
189
+      .register-logo {
190
+        width: 80px;
191
+        height: 80px;
192
+        margin-bottom: 20px;
193
+      }
194
+      
195
+      h2 {
196
+        color: #333;
197
+        font-size: 24px;
198
+        margin-bottom: 10px;
199
+      }
200
+      
201
+      p {
202
+        color: #666;
203
+        font-size: 14px;
204
+      }
205
+    }
206
+    
207
+    .register-form {
208
+      .register-button {
209
+        width: 100%;
210
+        height: 44px;
211
+        font-size: 16px;
212
+        border-radius: 6px;
213
+      }
214
+      
215
+      .register-links {
216
+        margin-top: 20px;
217
+        text-align: center;
218
+        
219
+        .link {
220
+          color: #666;
221
+          text-decoration: none;
222
+          font-size: 14px;
223
+          
224
+          &:hover {
225
+            color: #409EFF;
226
+          }
227
+        }
228
+      }
229
+    }
230
+  }
231
+}
232
+</style>

+ 293
- 0
frontend/src/views/profile/ProfileView.vue Zobrazit soubor

1
+<template>
2
+  <div class="profile-container">
3
+    <el-card class="profile-card">
4
+      <template #header>
5
+        <div class="profile-header">
6
+          <el-avatar :size="80" :src="userInfo.avatar || '/avatar.png'" />
7
+          <div class="profile-info">
8
+            <h2>{{ userInfo.nickname }}</h2>
9
+            <p>{{ userInfo.username }}</p>
10
+          </div>
11
+        </div>
12
+      </template>
13
+      
14
+      <el-tabs v-model="activeTab" class="profile-tabs">
15
+        <el-tab-pane label="基本信息" name="basic">
16
+          <el-form
17
+            ref="basicFormRef"
18
+            :model="basicForm"
19
+            :rules="basicRules"
20
+            label-width="100px"
21
+            class="basic-form"
22
+          >
23
+            <el-form-item label="用户名">
24
+              {{ userInfo.username }}
25
+            </el-form-item>
26
+            <el-form-item label="昵称" prop="nickname">
27
+              <el-input v-model="basicForm.nickname" />
28
+            </el-form-item>
29
+            <el-form-item label="邮箱" prop="email">
30
+              <el-input v-model="basicForm.email" />
31
+            </el-form-item>
32
+            <el-form-item label="手机号" prop="phone">
33
+              <el-input v-model="basicForm.phone" />
34
+            </el-form-item>
35
+            <el-form-item label="部门" prop="department">
36
+              <el-input v-model="basicForm.department" />
37
+            </el-form-item>
38
+            <el-form-item label="职位" prop="position">
39
+              <el-input v-model="basicForm.position" />
40
+            </el-form-item>
41
+            <el-form-item>
42
+              <el-button type="primary" @click="handleBasicUpdate">
43
+                更新基本信息
44
+              </el-button>
45
+            </el-form-item>
46
+          </el-form>
47
+        </el-tab-pane>
48
+        
49
+        <el-tab-pane label="修改密码" name="password">
50
+          <el-form
51
+            ref="passwordFormRef"
52
+            :model="passwordForm"
53
+            :rules="passwordRules"
54
+            label-width="100px"
55
+            class="password-form"
56
+          >
57
+            <el-form-item label="当前密码" prop="oldPassword">
58
+              <el-input
59
+                v-model="passwordForm.oldPassword"
60
+                type="password"
61
+                show-password
62
+                placeholder="请输入当前密码"
63
+              />
64
+            </el-form-item>
65
+            <el-form-item label="新密码" prop="newPassword">
66
+              <el-input
67
+                v-model="passwordForm.newPassword"
68
+                type="password"
69
+                show-password
70
+                placeholder="请输入新密码"
71
+              />
72
+            </el-form-item>
73
+            <el-form-item label="确认密码" prop="confirmPassword">
74
+              <el-input
75
+                v-model="passwordForm.confirmPassword"
76
+                type="password"
77
+                show-password
78
+                placeholder="请确认新密码"
79
+              />
80
+            </el-form-item>
81
+            <el-form-item>
82
+              <el-button type="primary" @click="handlePasswordUpdate">
83
+                修改密码
84
+              </el-button>
85
+            </el-form-item>
86
+          </el-form>
87
+        </el-tab-pane>
88
+        
89
+        <el-tab-pane label="操作日志" name="logs">
90
+          <el-timeline>
91
+            <el-timeline-item
92
+              v-for="log in operationLogs"
93
+              :key="log.id"
94
+              :timestamp="log.time"
95
+              placement="top"
96
+            >
97
+              <el-card>
98
+                <h4>{{ log.action }}</h4>
99
+                <p>{{ log.description }}</p>
100
+              </el-card>
101
+            </el-timeline-item>
102
+          </el-timeline>
103
+        </el-tab-pane>
104
+      </el-tabs>
105
+    </el-card>
106
+  </div>
107
+</template>
108
+
109
+<script setup lang="ts">
110
+import { ref, reactive, onMounted } from 'vue'
111
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
112
+
113
+const activeTab = ref('basic')
114
+const basicFormRef = ref<FormInstance>()
115
+const passwordFormRef = ref<FormInstance>()
116
+
117
+const userInfo = reactive({
118
+  username: '',
119
+  nickname: '',
120
+  avatar: '/avatar.png',
121
+  email: '',
122
+  phone: '',
123
+  department: '',
124
+  position: ''
125
+})
126
+
127
+const basicForm = reactive({
128
+  nickname: '',
129
+  email: '',
130
+  phone: '',
131
+  department: '',
132
+  position: ''
133
+})
134
+
135
+const passwordForm = reactive({
136
+  oldPassword: '',
137
+  newPassword: '',
138
+  confirmPassword: ''
139
+})
140
+
141
+const basicRules: FormRules = {
142
+  nickname: [
143
+    { required: true, message: '请输入昵称', trigger: 'blur' },
144
+    { min: 2, max: 10, message: '昵称长度为 2-10 个字符', trigger: 'blur' }
145
+  ],
146
+  email: [
147
+    { required: true, message: '请输入邮箱', trigger: 'blur' },
148
+    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
149
+  ],
150
+  phone: [
151
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
152
+  ]
153
+}
154
+
155
+const validateConfirmPassword = (rule: any, value: string, callback: any) => {
156
+  if (value === '') {
157
+    callback(new Error('请再次输入新密码'))
158
+  } else if (value !== passwordForm.newPassword) {
159
+    callback(new Error('两次输入密码不一致'))
160
+  } else {
161
+    callback()
162
+  }
163
+}
164
+
165
+const passwordRules: FormRules = {
166
+  oldPassword: [
167
+    { required: true, message: '请输入当前密码', trigger: 'blur' }
168
+  ],
169
+  newPassword: [
170
+    { required: true, message: '请输入新密码', trigger: 'blur' },
171
+    { min: 6, max: 20, message: '密码长度为 6-20 个字符', trigger: 'blur' }
172
+  ],
173
+  confirmPassword: [
174
+    { required: true, message: '请确认新密码', trigger: 'blur' },
175
+    { validator: validateConfirmPassword, trigger: 'blur' }
176
+  ]
177
+}
178
+
179
+const operationLogs = ref([
180
+  {
181
+    id: 1,
182
+    action: '登录系统',
183
+    description: '用户成功登录供水管理系统',
184
+    time: '2026-06-15 10:30:00'
185
+  },
186
+  {
187
+    id: 2,
188
+    action: '查看仪表盘',
189
+    description: '浏览了供水运营总览页面',
190
+    time: '2026-06-15 10:25:00'
191
+  },
192
+  {
193
+    id: 3,
194
+    action: '更新用户信息',
195
+    description: '修改了个人基本资料',
196
+    time: '2026-06-15 10:20:00'
197
+  }
198
+])
199
+
200
+const handleBasicUpdate = async () => {
201
+  if (!basicFormRef.value) return
202
+  
203
+  try {
204
+    const valid = await basicFormRef.value.validate()
205
+    if (valid) {
206
+      // 这里应该是更新用户信息的 API 调用
207
+      // const response = await updateUserInfo(basicForm)
208
+      
209
+      // 模拟更新成功
210
+      Object.assign(userInfo, basicForm)
211
+      ElMessage.success('基本信息更新成功')
212
+    }
213
+  } catch (error) {
214
+    console.error('基本信息更新失败:', error)
215
+  }
216
+}
217
+
218
+const handlePasswordUpdate = async () => {
219
+  if (!passwordFormRef.value) return
220
+  
221
+  try {
222
+    const valid = await passwordFormRef.value.validate()
223
+    if (valid) {
224
+      // 这里应该是修改密码的 API 调用
225
+      // const response = await changePassword(passwordForm)
226
+      
227
+      // 模拟修改成功
228
+      ElMessage.success('密码修改成功')
229
+      // 重置表单
230
+      passwordForm.oldPassword = ''
231
+      passwordForm.newPassword = ''
232
+      passwordForm.confirmPassword = ''
233
+    }
234
+  } catch (error) {
235
+    console.error('密码修改失败:', error)
236
+  }
237
+}
238
+
239
+onMounted(() => {
240
+  // 从 localStorage 获取用户信息
241
+  const userInfoStr = localStorage.getItem('userInfo')
242
+  if (userInfoStr) {
243
+    try {
244
+      const user = JSON.parse(userInfoStr)
245
+      Object.assign(userInfo, user)
246
+      Object.assign(basicForm, user)
247
+    } catch (e) {
248
+      console.error('解析用户信息失败:', e)
249
+    }
250
+  }
251
+})
252
+</script>
253
+
254
+<style scoped lang="scss">
255
+.profile-container {
256
+  padding: 20px;
257
+  
258
+  .profile-card {
259
+    max-width: 800px;
260
+    margin: 0 auto;
261
+    
262
+    .profile-header {
263
+      display: flex;
264
+      align-items: center;
265
+      gap: 20px;
266
+      
267
+      .profile-info {
268
+        h2 {
269
+          margin: 0 0 5px 0;
270
+          font-size: 20px;
271
+          color: #333;
272
+        }
273
+        
274
+        p {
275
+          margin: 0;
276
+          color: #666;
277
+          font-size: 14px;
278
+        }
279
+      }
280
+    }
281
+    
282
+    .profile-tabs {
283
+      margin-top: 20px;
284
+      
285
+      .basic-form,
286
+      .password-form {
287
+        max-width: 500px;
288
+        margin: 0 auto;
289
+      }
290
+    }
291
+  }
292
+}
293
+</style>

+ 518
- 0
frontend/src/views/system/user/UserList.vue Zobrazit soubor

1
+<template>
2
+  <div class="user-list">
3
+    <el-card class="filter-card">
4
+      <el-form :model="filterForm" inline>
5
+        <el-form-item label="用户名">
6
+          <el-input v-model="filterForm.username" placeholder="请输入用户名" clearable />
7
+        </el-form-item>
8
+        <el-form-item label="昵称">
9
+          <el-input v-model="filterForm.nickname" placeholder="请输入昵称" clearable />
10
+        </el-form-item>
11
+        <el-form-item label="状态">
12
+          <el-select v-model="filterForm.status" placeholder="请选择状态" clearable>
13
+            <el-option label="启用" :value="1" />
14
+            <el-option label="禁用" :value="0" />
15
+          </el-select>
16
+        </el-form-item>
17
+        <el-form-item label="部门">
18
+          <el-select v-model="filterForm.departmentId" placeholder="请选择部门" clearable>
19
+            <el-option
20
+              v-for="dept in departmentList"
21
+              :key="dept.id"
22
+              :label="dept.name"
23
+              :value="dept.id"
24
+            />
25
+          </el-select>
26
+        </el-form-item>
27
+        <el-form-item>
28
+          <el-button type="primary" @click="handleSearch">
29
+            <el-icon><Search /></el-icon>
30
+            搜索
31
+          </el-button>
32
+          <el-button @click="handleReset">
33
+            <el-icon><Refresh /></el-icon>
34
+            重置
35
+          </el-button>
36
+        </el-form-item>
37
+      </el-form>
38
+    </el-card>
39
+
40
+    <el-card class="table-card">
41
+      <template #header>
42
+        <div class="table-header">
43
+          <div class="left">
44
+            <el-button type="primary" @click="handleAdd">
45
+              <el-icon><Plus /></el-icon>
46
+              新增用户
47
+            </el-button>
48
+            <el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
49
+              <el-icon><Delete /></el-icon>
50
+              批量删除
51
+            </el-button>
52
+          </div>
53
+          <div class="right">
54
+            <el-button @click="handleExport">
55
+              <el-icon><Download /></el-icon>
56
+              导出
57
+            </el-button>
58
+            <el-button @click="handleRefresh">
59
+              <el-icon><Refresh /></el-icon>
60
+              刷新
61
+            </el-button>
62
+          </div>
63
+        </div>
64
+      </template>
65
+
66
+      <el-table
67
+        v-loading="loading"
68
+        :data="tableData"
69
+        @selection-change="handleSelectionChange"
70
+        style="width: 100%"
71
+      >
72
+        <el-table-column type="selection" width="55" />
73
+        <el-table-column prop="id" label="ID" width="80" />
74
+        <el-table-column prop="username" label="用户名" />
75
+        <el-table-column prop="nickname" label="昵称" />
76
+        <el-table-column prop="email" label="邮箱" />
77
+        <el-table-column prop="phone" label="手机号" />
78
+        <el-table-column prop="departmentName" label="部门" />
79
+        <el-table-column prop="position" label="职位" />
80
+        <el-table-column prop="status" label="状态" width="100">
81
+          <template #default="{ row }">
82
+            <el-tag :type="row.status === 1 ? 'success' : 'danger'">
83
+              {{ row.status === 1 ? '启用' : '禁用' }}
84
+            </el-tag>
85
+          </template>
86
+        </el-table-column>
87
+        <el-table-column prop="createTime" label="创建时间" width="180" />
88
+        <el-table-column label="操作" width="200" fixed="right">
89
+          <template #default="{ row }">
90
+            <el-button type="primary" size="small" @click="handleEdit(row)">
91
+              编辑
92
+            </el-button>
93
+            <el-button 
94
+              type="danger" 
95
+              size="small" 
96
+              :disabled="row.id === 1"
97
+              @click="handleDelete(row)"
98
+            >
99
+              删除
100
+            </el-button>
101
+          </template>
102
+        </el-table-column>
103
+      </el-table>
104
+
105
+      <div class="pagination">
106
+        <el-pagination
107
+          v-model:current-page="pagination.current"
108
+          v-model:page-size="pagination.size"
109
+          :page-sizes="[10, 20, 50, 100]"
110
+          :total="pagination.total"
111
+          layout="total, sizes, prev, pager, next, jumper"
112
+          @size-change="handleSizeChange"
113
+          @current-change="handleCurrentChange"
114
+        />
115
+      </div>
116
+    </el-card>
117
+
118
+    <!-- 用户表单对话框 -->
119
+    <el-dialog
120
+      v-model="dialogVisible"
121
+      :title="dialogTitle"
122
+      width="600px"
123
+      @close="handleDialogClose"
124
+    >
125
+      <el-form
126
+        ref="formRef"
127
+        :model="form"
128
+        :rules="rules"
129
+        label-width="100px"
130
+      >
131
+        <el-form-item label="用户名" prop="username">
132
+          <el-input v-model="form.username" placeholder="请输入用户名" />
133
+        </el-form-item>
134
+        <el-form-item label="密码" prop="password">
135
+          <el-input
136
+            v-model="form.password"
137
+            type="password"
138
+            placeholder="请输入密码"
139
+            show-password
140
+          />
141
+        </el-form-item>
142
+        <el-form-item label="确认密码" prop="confirmPassword">
143
+          <el-input
144
+            v-model="form.confirmPassword"
145
+            type="password"
146
+            placeholder="请确认密码"
147
+            show-password
148
+          />
149
+        </el-form-item>
150
+        <el-form-item label="昵称" prop="nickname">
151
+          <el-input v-model="form.nickname" placeholder="请输入昵称" />
152
+        </el-form-item>
153
+        <el-form-item label="邮箱" prop="email">
154
+          <el-input v-model="form.email" placeholder="请输入邮箱" />
155
+        </el-form-item>
156
+        <el-form-item label="手机号" prop="phone">
157
+          <el-input v-model="form.phone" placeholder="请输入手机号" />
158
+        </el-form-item>
159
+        <el-form-item label="部门" prop="departmentId">
160
+          <el-select v-model="form.departmentId" placeholder="请选择部门" style="width: 100%">
161
+            <el-option
162
+              v-for="dept in departmentList"
163
+              :key="dept.id"
164
+              :label="dept.name"
165
+              :value="dept.id"
166
+            />
167
+          </el-select>
168
+        </el-form-item>
169
+        <el-form-item label="职位" prop="position">
170
+          <el-input v-model="form.position" placeholder="请输入职位" />
171
+        </el-form-item>
172
+        <el-form-item label="状态" prop="status">
173
+          <el-radio-group v-model="form.status">
174
+            <el-radio :value="1">启用</el-radio>
175
+            <el-radio :value="0">禁用</el-radio>
176
+          </el-radio-group>
177
+        </el-form-item>
178
+      </el-form>
179
+
180
+      <template #footer>
181
+        <el-button @click="dialogVisible = false">取消</el-button>
182
+        <el-button type="primary" :loading="submitting" @click="handleSubmit">
183
+          确定
184
+        </el-button>
185
+      </template>
186
+    </el-dialog>
187
+  </div>
188
+</template>
189
+
190
+<script setup lang="ts">
191
+import { ref, reactive, onMounted } from 'vue'
192
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
193
+import { Search, Refresh, Plus, Delete, Download } from '@element-plus/icons-vue'
194
+import { userApi } from '@/api/system'
195
+
196
+// 搜索表单
197
+const filterForm = reactive({
198
+  username: '',
199
+  nickname: '',
200
+  status: undefined,
201
+  departmentId: undefined
202
+})
203
+
204
+// 表格数据
205
+const tableData = ref([])
206
+const loading = ref(false)
207
+const selectedRows = ref([])
208
+
209
+// 分页
210
+const pagination = reactive({
211
+  current: 1,
212
+  size: 20,
213
+  total: 0
214
+})
215
+
216
+// 部门列表
217
+const departmentList = ref([
218
+  { id: 1, name: '技术部' },
219
+  { id: 2, name: '运营部' },
220
+  { id: 3, name: '财务部' },
221
+  { id: 4, name: '人事部' }
222
+])
223
+
224
+// 对话框
225
+const dialogVisible = ref(false)
226
+const dialogTitle = ref('')
227
+const submitting = ref(false)
228
+
229
+// 表单
230
+const formRef = ref<FormInstance>()
231
+const form = reactive({
232
+  id: undefined,
233
+  username: '',
234
+  password: '',
235
+  confirmPassword: '',
236
+  nickname: '',
237
+  email: '',
238
+  phone: '',
239
+  departmentId: undefined,
240
+  position: '',
241
+  status: 1
242
+})
243
+
244
+// 表单验证规则
245
+const validateConfirmPassword = (rule: any, value: string, callback: any) => {
246
+  if (value === '') {
247
+    callback(new Error('请再次输入密码'))
248
+  } else if (value !== form.password) {
249
+    callback(new Error('两次输入密码不一致'))
250
+  } else {
251
+    callback()
252
+  }
253
+}
254
+
255
+const rules: FormRules = {
256
+  username: [
257
+    { required: true, message: '请输入用户名', trigger: 'blur' },
258
+    { min: 3, max: 20, message: '用户名长度为 3-20 个字符', trigger: 'blur' },
259
+    { pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线', trigger: 'blur' }
260
+  ],
261
+  password: [
262
+    { required: true, message: '请输入密码', trigger: 'blur' },
263
+    { min: 6, max: 20, message: '密码长度为 6-20 个字符', trigger: 'blur' },
264
+    { 
265
+      validator: (rule, value, callback) => {
266
+        if (form.id === undefined) {
267
+          // 新增时必须输入密码
268
+          if (!value) {
269
+            callback(new Error('请输入密码'))
270
+          }
271
+          callback()
272
+        } else {
273
+          // 编辑时密码可以为空
274
+          callback()
275
+        }
276
+      },
277
+      trigger: 'blur'
278
+    }
279
+  ],
280
+  confirmPassword: [
281
+    { validator: validateConfirmPassword, trigger: 'blur' }
282
+  ],
283
+  nickname: [
284
+    { required: true, message: '请输入昵称', trigger: 'blur' },
285
+    { min: 2, max: 10, message: '昵称长度为 2-10 个字符', trigger: 'blur' }
286
+  ],
287
+  email: [
288
+    { required: true, message: '请输入邮箱', trigger: 'blur' },
289
+    { type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
290
+  ],
291
+  phone: [
292
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
293
+  ],
294
+  departmentId: [
295
+    { required: true, message: '请选择部门', trigger: 'change' }
296
+  ]
297
+}
298
+
299
+// 获取用户列表
300
+const getUserList = async () => {
301
+  loading.value = true
302
+  try {
303
+    const params = {
304
+      page: pagination.current,
305
+      size: pagination.size,
306
+      ...filterForm
307
+    }
308
+    const response = await userApi.getList(params)
309
+    tableData.value = response.list || []
310
+    pagination.total = response.total || 0
311
+  } catch (error) {
312
+    console.error('获取用户列表失败:', error)
313
+    ElMessage.error('获取用户列表失败')
314
+  } finally {
315
+    loading.value = false
316
+  }
317
+}
318
+
319
+// 搜索
320
+const handleSearch = () => {
321
+  pagination.current = 1
322
+  getUserList()
323
+}
324
+
325
+// 重置
326
+const handleReset = () => {
327
+  filterForm.username = ''
328
+  filterForm.nickname = ''
329
+  filterForm.status = undefined
330
+  filterForm.departmentId = undefined
331
+  handleSearch()
332
+}
333
+
334
+// 刷新
335
+const handleRefresh = () => {
336
+  getUserList()
337
+}
338
+
339
+// 导出
340
+const handleExport = () => {
341
+  ElMessage.info('导出功能开发中')
342
+}
343
+
344
+// 选择行
345
+const handleSelectionChange = (selection: any[]) => {
346
+  selectedRows.value = selection
347
+}
348
+
349
+// 批量删除
350
+const handleBatchDelete = async () => {
351
+  try {
352
+    await ElMessageBox.confirm(
353
+      `确定要删除选中的 ${selectedRows.value.length} 个用户吗?`,
354
+      '确认删除',
355
+      {
356
+        type: 'warning'
357
+      }
358
+    )
359
+    
360
+    // 这里应该调用批量删除 API
361
+    const ids = selectedRows.value.map(item => item.id)
362
+    // await userApi.batchDelete(ids)
363
+    
364
+    ElMessage.success('批量删除成功')
365
+    getUserList()
366
+  } catch (error) {
367
+    if (error !== 'cancel') {
368
+      console.error('批量删除失败:', error)
369
+      ElMessage.error('批量删除失败')
370
+    }
371
+  }
372
+}
373
+
374
+// 新增用户
375
+const handleAdd = () => {
376
+  dialogTitle.value = '新增用户'
377
+  form.id = undefined
378
+  form.username = ''
379
+  form.password = ''
380
+  form.confirmPassword = ''
381
+  form.nickname = ''
382
+  form.email = ''
383
+  form.phone = ''
384
+  form.departmentId = undefined
385
+  form.position = ''
386
+  form.status = 1
387
+  dialogVisible.value = true
388
+}
389
+
390
+// 编辑用户
391
+const handleEdit = (row: any) => {
392
+  dialogTitle.value = '编辑用户'
393
+  form.id = row.id
394
+  form.username = row.username
395
+  form.password = ''
396
+  form.confirmPassword = ''
397
+  form.nickname = row.nickname
398
+  form.email = row.email
399
+  form.phone = row.phone
400
+  form.departmentId = row.departmentId
401
+  form.position = row.position
402
+  form.status = row.status
403
+  dialogVisible.value = true
404
+}
405
+
406
+// 删除用户
407
+const handleDelete = async (row: any) => {
408
+  try {
409
+    await ElMessageBox.confirm(
410
+      `确定要删除用户 "${row.username}" 吗?`,
411
+      '确认删除',
412
+      {
413
+        type: 'warning'
414
+      }
415
+    )
416
+    
417
+    await userApi.delete(row.id)
418
+    ElMessage.success('删除成功')
419
+    getUserList()
420
+  } catch (error) {
421
+    if (error !== 'cancel') {
422
+      console.error('删除失败:', error)
423
+      ElMessage.error('删除失败')
424
+    }
425
+  }
426
+}
427
+
428
+// 提交表单
429
+const handleSubmit = async () => {
430
+  if (!formRef.value) return
431
+  
432
+  try {
433
+    await formRef.value.validate()
434
+    submitting.value = true
435
+    
436
+    const isEdit = form.id !== undefined
437
+    const submitData = {
438
+      username: form.username,
439
+      nickname: form.nickname,
440
+      email: form.email,
441
+      phone: form.phone,
442
+      departmentId: form.departmentId,
443
+      position: form.position,
444
+      status: form.status
445
+    }
446
+    
447
+    if (form.password) {
448
+      submitData.password = form.password
449
+    }
450
+    
451
+    if (isEdit) {
452
+      await userApi.update(form.id, submitData)
453
+      ElMessage.success('更新成功')
454
+    } else {
455
+      await userApi.create(submitData)
456
+      ElMessage.success('创建成功')
457
+    }
458
+    
459
+    dialogVisible.value = false
460
+    getUserList()
461
+  } catch (error) {
462
+    console.error('提交失败:', error)
463
+    ElMessage.error('提交失败')
464
+  } finally {
465
+    submitting.value = false
466
+  }
467
+}
468
+
469
+// 对话框关闭
470
+const handleDialogClose = () => {
471
+  formRef.value?.resetFields()
472
+}
473
+
474
+// 分页大小变化
475
+const handleSizeChange = (size: number) => {
476
+  pagination.size = size
477
+  getUserList()
478
+}
479
+
480
+// 页码变化
481
+const handleCurrentChange = (current: number) => {
482
+  pagination.current = current
483
+  getUserList()
484
+}
485
+
486
+// 初始化
487
+onMounted(() => {
488
+  getUserList()
489
+})
490
+</script>
491
+
492
+<style scoped lang="scss">
493
+.user-list {
494
+  .filter-card {
495
+    margin-bottom: 20px;
496
+  }
497
+  
498
+  .table-card {
499
+    .table-header {
500
+      display: flex;
501
+      justify-content: space-between;
502
+      align-items: center;
503
+      
504
+      .left,
505
+      .right {
506
+        display: flex;
507
+        gap: 10px;
508
+      }
509
+    }
510
+  }
511
+  
512
+  .pagination {
513
+    margin-top: 20px;
514
+    display: flex;
515
+    justify-content: flex-end;
516
+  }
517
+}
518
+</style>

+ 33
- 7
frontend/tsconfig.json Zobrazit soubor

1
 {
1
 {
2
   "compilerOptions": {
2
   "compilerOptions": {
3
-    "target": "ESNext",
3
+    "target": "ES2020",
4
+    "useDefineForClassFields": true,
4
     "module": "ESNext",
5
     "module": "ESNext",
6
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+    "skipLibCheck": true,
8
+
5
     "moduleResolution": "bundler",
9
     "moduleResolution": "bundler",
6
-    "strict": true,
10
+    "allowImportingTsExtensions": true,
11
+    "resolveJsonModule": true,
12
+    "isolatedModules": true,
13
+    "noEmit": true,
7
     "jsx": "preserve",
14
     "jsx": "preserve",
15
+
16
+    "strict": true,
17
+    "noUnusedLocals": true,
18
+    "noUnusedParameters": true,
19
+    "noFallthroughCasesInSwitch": true,
20
+
21
+    /* Path mapping */
8
     "baseUrl": ".",
22
     "baseUrl": ".",
9
     "paths": {
23
     "paths": {
10
-      "@/*": [
11
-        "src/*"
12
-      ]
13
-    }
24
+      "@/*": ["./src/*"],
25
+      "@components/*": ["./src/components/*"],
26
+      "@views/*": ["./src/views/*"],
27
+      "@api/*": ["./src/api/*"],
28
+      "@utils/*": ["./src/utils/*"],
29
+      "@assets/*": ["./src/assets/*"]
30
+    },
31
+    
32
+    /* Type declaration files */
33
+    "types": ["element-plus/global"],
34
+    
35
+    /* Vue support */
36
+    "experimentalDecorators": true,
37
+    "emitDecoratorMetadata": true
14
   },
38
   },
15
   "include": [
39
   "include": [
16
     "src/**/*.ts",
40
     "src/**/*.ts",
17
     "src/**/*.d.ts",
41
     "src/**/*.d.ts",
42
+    "src/**/*.tsx",
18
     "src/**/*.vue"
43
     "src/**/*.vue"
19
-  ]
44
+  ],
45
+  "references": [{ "path": "./tsconfig.node.json" }]
20
 }
46
 }

+ 10
- 0
frontend/tsconfig.node.json Zobrazit soubor

1
+{
2
+  "compilerOptions": {
3
+    "composite": true,
4
+    "skipLibCheck": true,
5
+    "module": "ESNext",
6
+    "moduleResolution": "bundler",
7
+    "allowSyntheticDefaultImports": true
8
+  },
9
+  "include": ["vite.config.ts"]
10
+}

+ 79
- 7
frontend/vite.config.ts Zobrazit soubor

1
 import { defineConfig } from 'vite'
1
 import { defineConfig } from 'vite'
2
 import vue from '@vitejs/plugin-vue'
2
 import vue from '@vitejs/plugin-vue'
3
-import path from 'path'
3
+import AutoImport from 'unplugin-auto-import/vite'
4
+import Components from 'unplugin-vue-components/vite'
5
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
6
+import compression from 'vite-plugin-compression'
4
 
7
 
8
+// https://vitejs.dev/config/
5
 export default defineConfig({
9
 export default defineConfig({
6
-  plugins: [vue()],
7
-  resolve: { alias: { '@': path.resolve('src') } },
10
+  plugins: [
11
+    vue(),
12
+    AutoImport({
13
+      // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
14
+      imports: ['vue', 'vue-router', 'pinia'],
15
+      // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox 等
16
+      resolvers: [ElementPlusResolver()],
17
+      dts: 'src/auto-imports.d.ts',
18
+    }),
19
+    Components({
20
+      // 自动导入 Element Plus 组件
21
+      resolvers: [ElementPlusResolver()],
22
+      dts: 'src/components.d.ts',
23
+    }),
24
+    // 开启 Gzip 压缩
25
+    compression({
26
+      verbose: true,
27
+      disable: false,
28
+      threshold: 10240,
29
+      algorithm: 'gzip',
30
+      ext: '.gz',
31
+    }),
32
+  ],
33
+  resolve: {
34
+    alias: {
35
+      '@': '/src',
36
+      '@components': '/src/components',
37
+      '@views': '/src/views',
38
+      '@api': '/src/api',
39
+      '@utils': '/src/utils',
40
+      '@assets': '/src/assets',
41
+    },
42
+  },
43
+  css: {
44
+    preprocessorOptions: {
45
+      scss: {
46
+        additionalData: `@import "@/assets/style.scss";`,
47
+      },
48
+    },
49
+  },
8
   server: {
50
   server: {
9
     port: 3000,
51
     port: 3000,
52
+    open: true,
53
+    cors: true,
10
     proxy: {
54
     proxy: {
11
-      '/api': { target: 'http://localhost:8080', changeOrigin: true }
12
-    }
13
-  }
14
-})
55
+      '/api': {
56
+        target: 'http://localhost:8080',
57
+        changeOrigin: true,
58
+        rewrite: (path) => path.replace(/^\/api/, ''),
59
+      },
60
+    },
61
+  },
62
+  build: {
63
+    // 生产环境构建配置
64
+    target: 'es2015',
65
+    outDir: 'dist',
66
+    assetsDir: 'assets',
67
+    minify: 'terser',
68
+    terserOptions: {
69
+      compress: {
70
+        drop_console: true,
71
+        drop_debugger: true,
72
+      },
73
+    },
74
+    rollupOptions: {
75
+      output: {
76
+        chunkFileNames: 'assets/js/[name]-[hash].js',
77
+        entryFileNames: 'assets/js/[name]-[hash].js',
78
+        assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
79
+        manualChunks: {
80
+          'element-plus': ['element-plus'],
81
+          'vue-vendor': ['vue', 'vue-router', 'pinia'],
82
+        },
83
+      },
84
+    },
85
+  },
86
+})