commit
2cd0e67a5f
@ -0,0 +1,32 @@
|
||||
# 个人周计划-第11周
|
||||
|
||||
## 姓名和起止时间
|
||||
|
||||
**姓 名:** 曹峻茂
|
||||
**团队名称:** 软1-汪汪队
|
||||
**开始时间:** 2025-12-8
|
||||
**结束时间:** 2025-12-14
|
||||
|
||||
|
||||
## 本周任务计划安排
|
||||
| 序号 | 计划内容 | 协作人 | 情况说明 |
|
||||
|----|--------------------|----|-----------------------|
|
||||
| 1 | 确定分工 | 组员 | 2023-12-8 开会确定计划和团队分工 |
|
||||
|3|配置云服务器和数据库环境| 个人 |配置云服务器和数据库环境|
|
||||
|
||||
## 小结
|
||||
|
||||
|
||||
1. **知识储备:** 学习后续需要使用的知识,为后续的开发做准备;
|
||||
2. **文档撰写:** 完成迭代开发计划撰写。
|
||||
3. **项目管理** 管理项目环境和框架
|
||||
---
|
||||
|
||||
## 【注】
|
||||
|
||||
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
|
||||
1. 请将个人计划和总结提前发给负责人;
|
||||
1. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
|
||||
1. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台;
|
||||
|
||||
---
|
||||
@ -0,0 +1,30 @@
|
||||
# 个人周计划-第12周
|
||||
|
||||
## 姓名和起止时间
|
||||
|
||||
**姓 名:** [罗月航]
|
||||
**团队名称:** 1班-汪汪队
|
||||
**开始时间:** 2025-12-08
|
||||
**结束时间:** 2025-12-14
|
||||
|
||||
## 本周任务计划安排
|
||||
|
||||
| 序号 | 计划内容 | 协作人 | 情况说明 |
|
||||
| ---- | -------- | ------ | -------- |
|
||||
| 1 | 运维APP工单功能联调 | 后端开发 | 调试工单创建、抢单、处理、完成等完整业务流程接口 |
|
||||
| 2 | 运维APP设备监控联调 | 后端开发 | 调试设备数据实时获取、告警推送、状态更新等接口 |
|
||||
| 3 | 学生端扫码功能联调 | 后端开发 | 调试扫码用水、水质查询、设备识别等核心功能接口 |
|
||||
| 4 | 学生端用水记录联调 | 后端开发 | 调试个人用水量统计、历史记录查询、数据可视化接口 |
|
||||
| 5 | 联调问题记录与修复 | 后端开发 | 记录联调过程中的问题并协同修复,确保功能稳定 |
|
||||
|
||||
|
||||
## 小结
|
||||
|
||||
1. **联调协调**:需要与后端团队协调联调时间和环境,确保联调效率;
|
||||
2. **测试数据**:需要准备充分的测试数据覆盖各种业务场景;
|
||||
3. **问题追踪**:需要建立有效的问题追踪机制,确保问题及时解决;
|
||||
4. **性能关注**:在联调过程中需要关注接口响应时间和前端性能表现;
|
||||
5. **错误处理**:需要测试各种异常情况下的错误处理和用户提示;
|
||||
6. **移动端适配**:需要确保联调功能在不同移动设备上的兼容性;
|
||||
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
# 个人周总结-第十一周
|
||||
|
||||
## 基本信息
|
||||
|
||||
**姓 名:** 张红卫
|
||||
**团队名称:** 软1-汪汪队
|
||||
**起止时间:** 2025-12-1 至 2025-12-7
|
||||
**核心职责:** 前端界面开发 + 前后端联调支持
|
||||
|
||||
---
|
||||
|
||||
## 本周任务完成情况
|
||||
|
||||
| 序号 | 计划内容 | 完成状态 | 详细说明 |
|
||||
|------|------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------|
|
||||
| 1 | 完成 Web 端统计页面功能开发与交互优化 | 进行中 | 已完成 ECharts 图表框架集成,实现用水量、告警次数等基础图表展示;筛选功能已初步实现,接口数据对接正在调试中 |
|
||||
| 2 | 完成 Web 端设备管理页面功能开发与交互优化 | 进行中 | 页面结构与基础表单组件已完成,支持设备增删改查;批量操作功能正在开发中,接口调试同步进行 |
|
||||
| 3 | 参与前后端联调工作 | 进行中 | 与王磊、周竞由等后端成员保持每日沟通,统计模块接口已部分联调完成,设备管理模块接口正在逐步对接 |
|
||||
| 4 | 性能优化与异常处理完善 | 进行中 | 已引入分页加载机制优化大数据展示;权限控制逻辑与后端对齐中;导航栏异常跳转处理逻辑已优化并测试 |
|
||||
|
||||
---
|
||||
|
||||
## 本周总结
|
||||
|
||||
### 一、任务进展概述
|
||||
本周主要工作集中在 **统计页面** 和 **设备管理页面** 的前后端联调阶段。目前两个页面的基础功能框架已搭建完成,核心接口正在有序对接中。性能优化与权限控制方案已初步实施,但整体进度仍处于联调阶段,尚未完全完成。
|
||||
|
||||
### 二、技术实现与收获
|
||||
1. **性能优化实践**
|
||||
- 引入分页加载机制,初步缓解大数据渲染压力
|
||||
|
||||
2. **权限控制集成**
|
||||
- 配合后端权限接口,搭建前端路由权限控制基础框架
|
||||
- 完善导航栏跳转与操作权限校验逻辑
|
||||
|
||||
3. **复杂业务处理**
|
||||
- 实现设备管理中的复杂表单验证与状态管理
|
||||
- 掌握批量操作的前端实现方式
|
||||
|
||||
### 三、遇到的问题与挑战
|
||||
1. **接口响应延迟**
|
||||
- 部分统计查询接口响应时间较长,影响页面加载体验
|
||||
|
||||
2. **权限逻辑不一致**
|
||||
- 前端权限控制逻辑需与后端进一步对齐,部分页面权限拦截尚未生效
|
||||
|
||||
3. **性能瓶颈**
|
||||
- 大数据量图表渲染仍存在轻微卡顿,需进一步优化
|
||||
|
||||
4. **联调协作效率**
|
||||
- 接口变更沟通不够及时,影响前端调试进度
|
||||
|
||||
### 四、协作建议
|
||||
1. **接口规范明确**
|
||||
- 建议后端提供完整的接口文档与示例数据
|
||||
- 建立接口变更的版本管理机制
|
||||
|
||||
2. **联调流程优化**
|
||||
- 每周安排专门联调会议
|
||||
- 建立联调问题跟踪清单
|
||||
|
||||
3. **性能监控机制**
|
||||
- 共同建立接口性能监控体系
|
||||
- 定期进行性能测试与优化
|
||||
|
||||
### 五、工作总结与展望
|
||||
本周在前后端联调过程中,进一步深入理解了系统整体架构与业务流程。通过实际调试,不仅提升了问题排查能力,也对前端性能优化和权限控制有了更深刻的认识。虽然进度比预期稍慢,但通过持续的沟通协作,各项任务正在稳步推进。
|
||||
|
||||
**期待与建议:**
|
||||
- 希望团队成员继续加强沟通,及时同步进展和问题
|
||||
- 建议建立更规范的联调流程,提高协作效率
|
||||
- 期待后端接口性能进一步提升,为前端优化提供更好基础
|
||||
|
||||
---
|
||||
|
||||
**提交时间:** 2025-12-07
|
||||
**下周重点:** 完成所有接口联调,实现性能优化目标,完善权限控制系统
|
||||
@ -0,0 +1,22 @@
|
||||
# 个人周计划-第12周
|
||||
|
||||
## 姓名和起止时间
|
||||
|
||||
**姓 名:** 周竞由
|
||||
**团队名称:** 软1-汪汪队
|
||||
**开始时间:** 2025-12-8
|
||||
**结束时间:** 2025-12-14
|
||||
|
||||
## 本周任务计划安排
|
||||
| 序号 | 计划内容 | 协作人 | 情况说明 |
|
||||
|----|-----------------------------|-----|-------------------------------------|
|
||||
| 1 | 前后端接口联调:核心业务接口(权限/告警/工单)全量对接验证 | 前端协作人员 | 梳理接口文档并核对字段定义,逐一验证参数传递、数据返回格式、异常场景处理逻辑,修复接口调用异常问题 |
|
||||
| 2 | 页面与后端逻辑联调:权限展示/告警推送/工单流转页面 | 前端协作人员 | 联调各角色权限对应的页面展示逻辑,验证告警弹窗/列表与后端数据实时联动,确保工单全流程页面数据同步更新 |
|
||||
| 3 | 联调问题闭环与性能优化:接口响应/数据一致性/多端兼容 | 前端协作人员 | 汇总联调问题清单并逐一修复,完成回归测试;优化高频接口响应速度,验证多浏览器/移动设备兼容性 |
|
||||
|
||||
## 小结
|
||||
|
||||
1. **接口验证:** 完成核心业务接口全量联调,统一前后端字段定义与异常处理标准,解决接口调用类问题;
|
||||
2. **页面联调:** 实现权限、告警、工单相关页面与后端逻辑的精准联动,保障页面数据实时性与准确性;
|
||||
3. **问题闭环:** 梳理并修复联调全流程问题,通过回归测试验证问题解决效果,确保无遗留问题;
|
||||
4. **体验优化:** 针对高频接口做性能调优,提升页面交互响应速度,保障多端访问的兼容性与稳定性。
|
||||
@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
# app2
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
|
||||
```sh
|
||||
npm run test:unit
|
||||
```
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "app2",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.2",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"jsdom": "^27.2.0",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-vue-devtools": "^8.0.5",
|
||||
"vitest": "^4.0.14"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,86 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
After Width: | Height: | Size: 276 B |
@ -0,0 +1,138 @@
|
||||
/* 更新现有的 main.css */
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
'Inter',
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
.no-scroll {
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 移动端点击效果优化 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
* {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
button {
|
||||
font-size: 16px !important; /* 防止iOS缩放 */
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* 修复移动端overflow-scrolling问题 */
|
||||
.element
|
||||
@ -0,0 +1,37 @@
|
||||
/* 移动端全局样式 */
|
||||
:root {
|
||||
--mobile-width: 375px;
|
||||
--mobile-height: 667px;
|
||||
--primary-color: #1156b1;
|
||||
--secondary-color: #81d3f8;
|
||||
--success-color: #04d919;
|
||||
--warning-color: #ff9800;
|
||||
--danger-color: #f44336;
|
||||
}
|
||||
|
||||
/* 移动端基础样式 */
|
||||
#app {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 移动端容器 */
|
||||
.mobile-container {
|
||||
max-width: var(--mobile-width);
|
||||
min-height: var(--mobile-height);
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
.safe-area {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { mount } from '@vue/test-utils'
|
||||
import HelloWorld from '../HelloWorld.vue'
|
||||
|
||||
describe('HelloWorld', () => {
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
|
||||
expect(wrapper.text()).toContain('Hello Vitest')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
@ -0,0 +1,15 @@
|
||||
import './assets/main.css'
|
||||
import './assets/mobile.css' // 添加这行
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@ -0,0 +1,39 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'StudentLoginPage',
|
||||
component: () => import('../views/StudentLoginPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'HomePage',
|
||||
component: () => import('../views/HomePage.vue')
|
||||
},
|
||||
{
|
||||
path: '/water-quality',
|
||||
name: 'WaterQuality',
|
||||
component: () => import('../views/WaterQualityPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/scan',
|
||||
name: 'ScanPage',
|
||||
component: () => import('../views/ScanPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'ProfilePage',
|
||||
component: () => import('../views/ProfilePage.vue')
|
||||
},
|
||||
{
|
||||
path: '/history',
|
||||
name: 'HistoryPage',
|
||||
component: () => import('../views/HistoryPage.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -0,0 +1,11 @@
|
||||
// src/services/api.js
|
||||
import axios from 'axios'
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://localhost:8080', // Adjust to your backend URL
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
export default apiClient
|
||||
@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -0,0 +1,14 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||
},
|
||||
}),
|
||||
)
|
||||
@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=http://localhost:8080
|
||||
VITE_APP_ORIGIN=http://localhost:5173
|
||||
@ -1,58 +1,61 @@
|
||||
import type { LoginRequest, LoginVO } from './types/auth'
|
||||
// 替换原文件内容
|
||||
import type { LoginRequest, LoginResponse, LoginVO } from './types/auth'
|
||||
|
||||
// 真实的登录API调用
|
||||
export const realLoginApi = async (data: LoginRequest): Promise<LoginVO> => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
|
||||
|
||||
console.log('🌐 调用登录接口:', `${API_BASE_URL}/api/common/login`)
|
||||
console.log('📤 请求数据:', data)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/common/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
console.log('📥 响应状态:', response.status, response.statusText)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`网络请求失败: ${response.status} ${response.statusText}`)
|
||||
export const realLoginApi = async (data: LoginRequest): Promise<LoginResponse> => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
|
||||
|
||||
console.log('🌐 调用登录接口:', `${API_BASE_URL}/api/common/login`)
|
||||
console.log('📤 请求数据:', data)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/common/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
console.log('📥 响应状态:', response.status, response.statusText)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('❌ 响应内容:', errorText)
|
||||
throw new Error(`网络请求失败: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result: LoginResponse = await response.json()
|
||||
console.log('✅ 登录响应:', result)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 登录接口调用失败:', error)
|
||||
throw new Error(`登录失败: ${error.message}`)
|
||||
}
|
||||
|
||||
const result: LoginVO = await response.json()
|
||||
console.log('✅ 登录响应:', result)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ 登录接口调用失败:', error)
|
||||
throw new Error(`登录失败: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 备用模拟登录
|
||||
export const mockLoginApi = async (data: LoginRequest): Promise<LoginVO> => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (data.username === 'admin' && data.password === '123456') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
token: 'mock-jwt-token-' + Date.now(),
|
||||
userInfo: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
realName: '张管理员',
|
||||
role: 'admin',
|
||||
avatar: ''
|
||||
export const mockLoginApi = async (data: LoginRequest): Promise<LoginResponse> => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (data.username === 'admin' && data.password === '123456') {
|
||||
return {
|
||||
code: 200,
|
||||
message: '登录成功',
|
||||
data: {
|
||||
token: 'mock-jwt-token-' + Date.now(),
|
||||
userInfo: {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
realName: '张管理员',
|
||||
role: 'admin',
|
||||
avatar: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error('用户名或密码错误')
|
||||
}
|
||||
} else {
|
||||
throw new Error('用户名或密码错误')
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
// src/api/deviceStatus.ts
|
||||
import axios from 'axios'
|
||||
|
||||
export const DeviceStatusApi = {
|
||||
// 获取设备状态列表 - 修改为匹配后端实际接口
|
||||
getDevicesByStatus: async (status: string, areaId?: string, deviceType?: string) => {
|
||||
try {
|
||||
const params: any = { status }
|
||||
if (areaId) params.areaId = areaId
|
||||
if (deviceType) params.deviceType = deviceType
|
||||
|
||||
const response = await axios.get('/api/web/device-status/by-status', { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw new Error(`获取设备列表失败: ${error}`)
|
||||
}
|
||||
},
|
||||
|
||||
// 标记设备在线
|
||||
markDeviceOnline: async (deviceId: string) => {
|
||||
try {
|
||||
const response = await axios.post(`/api/web/device-status/${deviceId}/online`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw new Error(`设置设备在线失败: ${error}`)
|
||||
}
|
||||
},
|
||||
|
||||
// 标记设备离线
|
||||
markDeviceOffline: async (deviceId: string, reason?: string) => {
|
||||
try {
|
||||
const params = reason ? { reason } : {}
|
||||
const response = await axios.post(`/api/web/device-status/${deviceId}/offline`, null, { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw new Error(`设置设备离线失败: ${error}`)
|
||||
}
|
||||
},
|
||||
|
||||
// 标记设备故障
|
||||
markDeviceFault: async (deviceId: string, faultType: string, description: string) => {
|
||||
try {
|
||||
const params = { faultType, description }
|
||||
const response = await axios.post(`/api/web/device-status/${deviceId}/fault`, null, { params })
|
||||
return response.data
|
||||
} catch (error) {
|
||||
throw new Error(`设置设备故障失败: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
// src/api/modules/auth.ts
|
||||
import { api } from '../request'
|
||||
import type { LoginRequest, LoginVO, ResultVO } from '../types/auth'
|
||||
|
||||
class AuthApi {
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
async login(data: LoginRequest): Promise<ResultVO<LoginVO>> {
|
||||
console.log('🚀 调用登录接口:', data)
|
||||
return api.post<ResultVO<LoginVO>>('/api/common/login', data)
|
||||
}
|
||||
}
|
||||
|
||||
export const authApi = new AuthApi()
|
||||
@ -1,22 +1,29 @@
|
||||
// 与后端 LoginRequest 对应的类型
|
||||
// src/api/types/auth.ts
|
||||
|
||||
// 登录请求参数 - 匹配后端的 LoginRequest
|
||||
export interface LoginRequest {
|
||||
username: string
|
||||
password: string
|
||||
rememberMe?: boolean
|
||||
username: string
|
||||
password: string
|
||||
userType: string // 添加这个属性
|
||||
rememberMe?: boolean
|
||||
}
|
||||
|
||||
// 通用响应结构
|
||||
export interface ResultVO<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 与后端 LoginVO 对应的类型
|
||||
// 登录响应数据 - 匹配后端的 LoginVO
|
||||
export interface LoginVO {
|
||||
code: number
|
||||
message: string
|
||||
data: {
|
||||
token: string
|
||||
userInfo: {
|
||||
id: number
|
||||
username: string
|
||||
realName: string
|
||||
role: string
|
||||
avatar?: string
|
||||
id: number
|
||||
username: string
|
||||
realName?: string // 根据后端字段调整
|
||||
role?: string // 根据后端字段调整
|
||||
userType?: string // 添加这个字段
|
||||
avatar?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +1,251 @@
|
||||
// src/router/index.ts
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHistory, type NavigationGuardNext, type RouteLocationNormalized } from 'vue-router'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import MainLayout from '../components/layout/MainLayout.vue' // 导入布局组件
|
||||
import MainLayout from '../components/layout/MainLayout.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'login',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
component: MainLayout, // 使用 MainLayout 作为布局
|
||||
children: [
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: () => import('../views/Dashboard.vue') // Dashboard 作为子路由
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('../views/AboutView.vue')
|
||||
},
|
||||
],
|
||||
path: '/',
|
||||
name: 'login',
|
||||
component: LoginView,
|
||||
meta: {
|
||||
title: '登录',
|
||||
requiresAuth: false // 不需要认证
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
component: MainLayout,
|
||||
meta: {
|
||||
requiresAuth: true // 需要认证
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'home',
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
meta: {
|
||||
title: '首页'
|
||||
}
|
||||
},
|
||||
// 设备监控相关路由
|
||||
{
|
||||
path: 'equipment',
|
||||
name: 'equipment',
|
||||
component: () => import('../views/equipment/EquipmentView.vue'),
|
||||
meta: {
|
||||
title: '设备监控'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'equipment/water-maker',
|
||||
name: 'water-maker',
|
||||
component: () => import('../views/equipment/WaterMaker.vue'),
|
||||
meta: {
|
||||
title: '制水设备'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'equipment/water-supplier',
|
||||
name: 'water-supplier',
|
||||
component: () => import('../views/equipment/WaterSupplier.vue'),
|
||||
meta: {
|
||||
title: '供水设备'
|
||||
}
|
||||
},
|
||||
// 工单管理相关路由
|
||||
{
|
||||
path: 'work-order',
|
||||
name: 'work-order',
|
||||
component: () => import('../views/workorder/WorkOrderView.vue'),
|
||||
meta: {
|
||||
title: '工单管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'work-order/pending',
|
||||
name: 'work-order-pending',
|
||||
component: () => import('../views/workorder/Pending.vue'),
|
||||
meta: {
|
||||
title: '待处理工单'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'work-order/timeout',
|
||||
name: 'work-order-timeout',
|
||||
component: () => import('../views/workorder/Timeout.vue'),
|
||||
meta: {
|
||||
title: '超时工单'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'work-order/processing',
|
||||
name: 'work-order-processing',
|
||||
component: () => import('../views/workorder/Processing.vue'),
|
||||
meta: {
|
||||
title: '处理中工单'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'work-order/review',
|
||||
name: 'work-order-review',
|
||||
component: () => import('../views/workorder/Review.vue'),
|
||||
meta: {
|
||||
title: '待审核工单'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'work-order/review/:id',
|
||||
name: 'ReviewDetail',
|
||||
component: () => import('../views/workorder/ReviewDetail.vue'),
|
||||
meta: {
|
||||
title: '工单审核详情'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'work-order/completed',
|
||||
name: 'work-order-completed',
|
||||
component: () => import('../views/workorder/Completed.vue'),
|
||||
meta: {
|
||||
title: '已完成工单'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/home/work-order/completed/:id',
|
||||
name: 'CompletedDetail',
|
||||
component: () => import('@/views/workorder/CompletedDetail.vue'),
|
||||
meta: {
|
||||
title: '结单信息'
|
||||
}
|
||||
},
|
||||
// 人员管理相关路由
|
||||
{
|
||||
path: 'personnel/admin',
|
||||
name: 'personnel-admin',
|
||||
component: () => import('../views/personnel/Admin.vue'),
|
||||
meta: {
|
||||
title: '管理员管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'personnel/maintenance',
|
||||
name: 'personnel-maintenance',
|
||||
component: () => import('../views/personnel/Maintenance.vue'),
|
||||
meta: {
|
||||
title: '运维人员管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'personnel/user',
|
||||
name: 'personnel-user',
|
||||
component: () => import('../views/personnel/User.vue'),
|
||||
meta: {
|
||||
title: '用户管理'
|
||||
}
|
||||
},
|
||||
// 片区相关路由
|
||||
{
|
||||
path: 'area/urban',
|
||||
name: 'area-urban',
|
||||
component: () => import('../views/area/Urban.vue'),
|
||||
meta: {
|
||||
title: '城市片区'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'equipment/water-maker/:id',
|
||||
name: 'water-maker-detail',
|
||||
component: () => import('../views/equipment/WaterMakerDetail.vue'),
|
||||
meta: {
|
||||
title: '制水设备详情'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'area/campus',
|
||||
name: 'area-campus',
|
||||
component: () => import('../views/area/Campus.vue'),
|
||||
meta: {
|
||||
title: '校园片区'
|
||||
}
|
||||
},
|
||||
// 个人信息路由
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'profile',
|
||||
component: () => import('../views/Profile.vue'),
|
||||
meta: {
|
||||
title: '个人信息',
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
meta: {
|
||||
title: '关于',
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
/* // 404 页面
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'NotFound',
|
||||
component: () => import('../views/NotFound.vue'),
|
||||
meta: {
|
||||
title: '页面不存在',
|
||||
requiresAuth: false
|
||||
}
|
||||
}*/
|
||||
]
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach(async (to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) => {
|
||||
// 设置页面标题
|
||||
const title = to.meta?.title as string || '校园直饮水管理系统'
|
||||
document.title = title
|
||||
|
||||
// 获取认证状态
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 初始化登录状态
|
||||
if (!authStore.isLoggedIn) {
|
||||
authStore.initialize()
|
||||
}
|
||||
|
||||
// 判断是否需要认证
|
||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
|
||||
|
||||
// 如果路由需要认证但用户未登录
|
||||
if (requiresAuth && !authStore.isLoggedIn) {
|
||||
// 重定向到登录页面,并保存当前想要访问的路径
|
||||
next({
|
||||
name: 'login',
|
||||
query: { redirect: to.fullPath }
|
||||
})
|
||||
}
|
||||
// 如果用户已登录但访问登录页面
|
||||
else if (to.name === 'login' && authStore.isLoggedIn) {
|
||||
// 检查是否有重定向路径
|
||||
const redirect = from.query.redirect as string || '/home'
|
||||
next(redirect)
|
||||
}
|
||||
// 其他情况正常放行
|
||||
else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
// 路由后置守卫 - 可在这里添加一些统计或清理工作
|
||||
router.afterEach((to: RouteLocationNormalized, from: RouteLocationNormalized) => {
|
||||
// 可以在这里添加页面访问统计等
|
||||
console.log(`路由跳转: ${from.fullPath} -> ${to.fullPath}`)
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -0,0 +1,7 @@
|
||||
<!-- src/views/equipment/EquipmentView.vue -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>设备监控</h1>
|
||||
<p>请选择左侧子菜单查看具体设备信息</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,412 @@
|
||||
<!-- src/views/equipment/WaterSupplier.vue -->
|
||||
<template>
|
||||
<div class="water-supplier-page">
|
||||
<!-- 页面标题和面包屑 -->
|
||||
<div class="page-header">
|
||||
<h2>供水机管理</h2>
|
||||
<div class="breadcrumb">校园矿化水平台 / 设备监控 / 供水机</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区 -->
|
||||
<div class="action-bar">
|
||||
<button class="btn-add">添加供水机</button>
|
||||
|
||||
<div class="filters">
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索设备ID或位置..."
|
||||
v-model="searchKeyword"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<button class="search-btn">搜索</button>
|
||||
</div>
|
||||
|
||||
<!-- 片区筛选 -->
|
||||
<select
|
||||
v-model="selectedArea"
|
||||
class="filter-select"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<option value="">全部片区</option>
|
||||
<option value="市区">市区</option>
|
||||
<option value="校区">校区</option>
|
||||
</select>
|
||||
|
||||
<!-- 状态筛选 -->
|
||||
<select
|
||||
v-model="selectedStatus"
|
||||
class="filter-select"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="online">在线</option>
|
||||
<option value="offline">离线</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="error">故障</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备表格 - 新增设备机型列 -->
|
||||
<div class="card">
|
||||
<table class="equipment-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备ID</th>
|
||||
<th>设备机型</th> <!-- 新增机型列 -->
|
||||
<th>所属片区</th>
|
||||
<th>详细位置</th>
|
||||
<th>状态</th>
|
||||
<th>最后上传时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="device in filteredDevices" :key="device.id">
|
||||
<td>{{ device.id }}</td>
|
||||
<td>供水机</td> <!-- 固定显示供水机机型 -->
|
||||
<td>{{ device.area }}</td>
|
||||
<td>{{ device.location }}</td>
|
||||
<td>
|
||||
<span :class="`status-tag ${device.status}`">
|
||||
{{ formatStatus(device.status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ device.lastUploadTime }}</td>
|
||||
<td class="operation-buttons">
|
||||
<button class="btn-view" @click="viewDevice(device.id)">查看详情</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="filteredDevices.length === 0">
|
||||
<td colspan="7" class="no-data">暂无设备数据</td> <!-- colspan从6改为7 -->
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination">
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="page-info">
|
||||
第 {{ currentPage }} 页 / 共 {{ totalPages }} 页
|
||||
</span>
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="currentPage++"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 设备状态类型定义
|
||||
type DeviceStatus = 'online' | 'offline' | 'warning' | 'error'
|
||||
|
||||
// 设备数据接口
|
||||
interface WaterSupplierDevice {
|
||||
id: string
|
||||
area: string
|
||||
location: string
|
||||
status: DeviceStatus
|
||||
lastUploadTime: string
|
||||
}
|
||||
|
||||
// 模拟供水机设备数据
|
||||
const waterSupplierDevices: WaterSupplierDevice[] = [
|
||||
{
|
||||
id: 'WS-2023-001',
|
||||
area: '市区',
|
||||
location: '行政中心大楼1楼大厅',
|
||||
status: 'online',
|
||||
lastUploadTime: '2023-10-25 10:15:33'
|
||||
},
|
||||
{
|
||||
id: 'WS-2023-002',
|
||||
area: '校区',
|
||||
location: '研究生公寓3号楼一层',
|
||||
status: 'online',
|
||||
lastUploadTime: '2023-10-25 09:30:22'
|
||||
},
|
||||
{
|
||||
id: 'WS-2023-003',
|
||||
area: '市区',
|
||||
location: '科技园区A座大厅',
|
||||
status: 'warning',
|
||||
lastUploadTime: '2023-10-25 08:45:11'
|
||||
},
|
||||
{
|
||||
id: 'WS-2023-004',
|
||||
area: '校区',
|
||||
location: '留学生公寓1楼',
|
||||
status: 'offline',
|
||||
lastUploadTime: '2023-10-24 23:05:47'
|
||||
},
|
||||
{
|
||||
id: 'WS-2023-005',
|
||||
area: '市区',
|
||||
location: '图书馆新馆2楼',
|
||||
status: 'error',
|
||||
lastUploadTime: '2023-10-25 07:20:35'
|
||||
}
|
||||
]
|
||||
|
||||
// 响应式数据
|
||||
const devices = ref<WaterSupplierDevice[]>(waterSupplierDevices)
|
||||
const searchKeyword = ref('')
|
||||
const selectedArea = ref('') // 片区筛选值
|
||||
const selectedStatus = ref('') // 状态筛选值
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 10 // 每页显示数量
|
||||
const router = useRouter()
|
||||
|
||||
// 多条件过滤设备数据
|
||||
const filteredDevices = computed(() => {
|
||||
return devices.value.filter(device => {
|
||||
const keywordMatch = searchKeyword.value.trim() === '' ||
|
||||
device.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
device.location.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
|
||||
const areaMatch = selectedArea.value === '' || device.area === selectedArea.value
|
||||
const statusMatch = selectedStatus.value === '' || device.status === selectedStatus.value
|
||||
|
||||
return keywordMatch && areaMatch && statusMatch
|
||||
})
|
||||
})
|
||||
|
||||
// 分页计算
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(filteredDevices.value.length / pageSize)
|
||||
})
|
||||
|
||||
// 状态格式化
|
||||
const formatStatus = (status: DeviceStatus): string => {
|
||||
const statusMap = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
warning: '警告',
|
||||
error: '故障'
|
||||
}
|
||||
return statusMap[status]
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDevice = (id: string) => {
|
||||
router.push(`/home/equipment/water-supplier/${id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 样式与制水机页面保持一致 */
|
||||
.water-supplier-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: #42b983;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: #359e75;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.equipment-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.equipment-table th,
|
||||
.equipment-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.equipment-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.equipment-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-tag.online {
|
||||
background-color: #e6f7ee;
|
||||
color: #00875a;
|
||||
}
|
||||
|
||||
.status-tag.offline {
|
||||
background-color: #f5f5f5;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.status-tag.warning {
|
||||
background-color: #fff7e6;
|
||||
color: #d48806;
|
||||
}
|
||||
|
||||
.status-tag.error {
|
||||
background-color: #ffebe6;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.operation-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.operation-buttons button {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.operation-buttons button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box, .filter-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,371 @@
|
||||
<!-- src/views/personnel/Admin.vue -->
|
||||
<template>
|
||||
<div class="admin-page">
|
||||
<!-- 页面标题和面包屑 -->
|
||||
<div class="page-header">
|
||||
<h2>管理员管理</h2>
|
||||
<div class="breadcrumb">校园矿化水平台 / 人员管理 / 管理员</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区 -->
|
||||
<div class="action-bar">
|
||||
<button class="btn-add" @click="handleAddAdmin">新增管理员</button>
|
||||
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索姓名或账号..."
|
||||
v-model="searchKeyword"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<button class="search-btn" @click="handleSearch">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 管理员表格 -->
|
||||
<div class="card">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>姓名</th>
|
||||
<th>账号</th>
|
||||
<th>联系电话</th>
|
||||
<th>身份</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="admin in filteredAdmins" :key="admin.id">
|
||||
<td>{{ admin.name }}</td>
|
||||
<td>{{ admin.account }}</td>
|
||||
<td>{{ admin.phone }}</td>
|
||||
<td>{{ admin.role }}</td>
|
||||
<td>
|
||||
<span :class="`status-tag ${admin.status}`">
|
||||
{{ admin.status === 'active' ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="operation-buttons">
|
||||
<button
|
||||
class="btn-edit"
|
||||
@click="handleEdit(admin.id)"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
class="btn-status"
|
||||
:class="admin.status === 'active' ? 'btn-disable' : 'btn-enable'"
|
||||
@click="handleStatusChange(admin.id, admin.status)"
|
||||
>
|
||||
{{ admin.status === 'active' ? '禁用' : '启用' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="filteredAdmins.length === 0">
|
||||
<td colspan="6" class="no-data">暂无管理员数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination">
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="page-info">
|
||||
第 {{ currentPage }} 页 / 共 {{ totalPages }} 页
|
||||
</span>
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="currentPage++"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 管理员状态类型
|
||||
type AdminStatus = 'active' | 'disabled'
|
||||
|
||||
// 管理员数据接口
|
||||
interface Admin {
|
||||
id: string
|
||||
name: string
|
||||
account: string
|
||||
phone: string
|
||||
role: string
|
||||
status: AdminStatus
|
||||
}
|
||||
|
||||
// 模拟管理员数据
|
||||
const adminList: Admin[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '张三',
|
||||
account: 'admin01',
|
||||
phone: '13800138000',
|
||||
role: '超级管理员',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '李四',
|
||||
account: 'admin02',
|
||||
phone: '13900139000',
|
||||
role: '设备管理员',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '王五',
|
||||
account: 'admin03',
|
||||
phone: '13700137000',
|
||||
role: '系统管理员',
|
||||
status: 'disabled'
|
||||
}
|
||||
]
|
||||
|
||||
// 响应式数据
|
||||
const admins = ref<Admin[]>(adminList)
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 10 // 每页显示数量
|
||||
const router = useRouter()
|
||||
|
||||
// 筛选后的管理员列表
|
||||
const filteredAdmins = computed(() => {
|
||||
return admins.value.filter(admin => {
|
||||
const keywordMatch = searchKeyword.value.trim() === '' ||
|
||||
admin.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
admin.account.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
return keywordMatch
|
||||
})
|
||||
})
|
||||
|
||||
// 分页计算
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(filteredAdmins.value.length / pageSize)
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
}
|
||||
|
||||
// 状态变更处理
|
||||
const handleStatusChange = (id: string, currentStatus: AdminStatus) => {
|
||||
const newStatus: AdminStatus = currentStatus === 'active' ? 'disabled' : 'active'
|
||||
admins.value = admins.value.map(admin =>
|
||||
admin.id === id ? { ...admin, status: newStatus } : admin
|
||||
)
|
||||
// 实际项目中这里应该调用API更新状态
|
||||
}
|
||||
|
||||
// 编辑处理
|
||||
const handleEdit = (id: string) => {
|
||||
router.push(`/home/personnel/admin/edit/${id}`)
|
||||
}
|
||||
|
||||
// 新增管理员
|
||||
const handleAddAdmin = () => {
|
||||
router.push('/home/personnel/admin/add')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: #42b983;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: #359e75;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.admin-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.admin-table th,
|
||||
.admin-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.admin-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.admin-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-tag.active {
|
||||
background-color: #e6f7ee;
|
||||
color: #00875a;
|
||||
}
|
||||
|
||||
.status-tag.disabled {
|
||||
background-color: #f5f5f5;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.operation-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.operation-buttons button {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.operation-buttons button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.btn-enable {
|
||||
background-color: #e6f7ee;
|
||||
color: #00875a;
|
||||
}
|
||||
|
||||
.btn-disable {
|
||||
background-color: #ffebe6;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,387 @@
|
||||
<!-- src/views/personnel/Maintenance.vue -->
|
||||
<template>
|
||||
<div class="maintenance-page">
|
||||
<!-- 页面标题和面包屑 -->
|
||||
<div class="page-header">
|
||||
<h2>维修人员管理</h2>
|
||||
<div class="breadcrumb">校园矿化水平台 / 人员管理 / 维修人员</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区 -->
|
||||
<div class="action-bar">
|
||||
<button class="btn-add" @click="handleAddMaintenance">新增维修人员</button>
|
||||
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索姓名或账号..."
|
||||
v-model="searchKeyword"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<button class="search-btn" @click="handleSearch">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 维修人员表格 -->
|
||||
<div class="card">
|
||||
<table class="maintenance-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>姓名</th>
|
||||
<th>账号</th>
|
||||
<th>联系电话</th>
|
||||
<th>维修片区</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="staff in filteredStaff" :key="staff.id">
|
||||
<td>{{ staff.name }}</td>
|
||||
<td>{{ staff.account }}</td>
|
||||
<td>{{ staff.phone }}</td>
|
||||
<td>{{ staff.area }}</td>
|
||||
<td>
|
||||
<span :class="`status-tag ${staff.status}`">
|
||||
{{ staff.status === 'active' ? '启用' : '禁用' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="operation-buttons">
|
||||
<button
|
||||
class="btn-view"
|
||||
@click="handleViewRecords(staff.id)"
|
||||
>
|
||||
查看维修记录
|
||||
</button>
|
||||
<button
|
||||
class="btn-edit"
|
||||
@click="handleEdit(staff.id)"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
class="btn-status"
|
||||
:class="staff.status === 'active' ? 'btn-disable' : 'btn-enable'"
|
||||
@click="handleStatusChange(staff.id, staff.status)"
|
||||
>
|
||||
{{ staff.status === 'active' ? '禁用' : '启用' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="filteredStaff.length === 0">
|
||||
<td colspan="6" class="no-data">暂无维修人员数据</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页控件 -->
|
||||
<div class="pagination">
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="currentPage--"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span class="page-info">
|
||||
第 {{ currentPage }} 页 / 共 {{ totalPages }} 页
|
||||
</span>
|
||||
<button
|
||||
class="page-btn"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="currentPage++"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// 维修人员状态类型
|
||||
type StaffStatus = 'active' | 'disabled'
|
||||
|
||||
// 维修人员数据接口
|
||||
interface MaintenanceStaff {
|
||||
id: string
|
||||
name: string
|
||||
account: string
|
||||
phone: string
|
||||
area: string
|
||||
status: StaffStatus
|
||||
}
|
||||
|
||||
// 模拟维修人员数据
|
||||
const staffList: MaintenanceStaff[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: '赵六',
|
||||
account: 'repair01',
|
||||
phone: '13500135000',
|
||||
area: '市区',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '孙七',
|
||||
account: 'repair02',
|
||||
phone: '13600136000',
|
||||
area: '校区',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '周八',
|
||||
account: 'repair03',
|
||||
phone: '13400134000',
|
||||
area: '市区',
|
||||
status: 'disabled'
|
||||
}
|
||||
]
|
||||
|
||||
// 响应式数据
|
||||
const staff = ref<MaintenanceStaff[]>(staffList)
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = 10 // 每页显示数量
|
||||
const router = useRouter()
|
||||
|
||||
// 筛选后的维修人员列表
|
||||
const filteredStaff = computed(() => {
|
||||
return staff.value.filter(person => {
|
||||
const keywordMatch = searchKeyword.value.trim() === '' ||
|
||||
person.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
|
||||
person.account.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
return keywordMatch
|
||||
})
|
||||
})
|
||||
|
||||
// 分页计算
|
||||
const totalPages = computed(() => {
|
||||
return Math.ceil(filteredStaff.value.length / pageSize)
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
currentPage.value = 1 // 重置到第一页
|
||||
}
|
||||
|
||||
// 状态变更处理
|
||||
const handleStatusChange = (id: string, currentStatus: StaffStatus) => {
|
||||
const newStatus: StaffStatus = currentStatus === 'active' ? 'disabled' : 'active'
|
||||
staff.value = staff.value.map(person =>
|
||||
person.id === id ? { ...person, status: newStatus } : person
|
||||
)
|
||||
// 实际项目中这里应该调用API更新状态
|
||||
}
|
||||
|
||||
// 编辑处理
|
||||
const handleEdit = (id: string) => {
|
||||
router.push(`/home/personnel/maintenance/edit/${id}`)
|
||||
}
|
||||
|
||||
// 查看维修记录
|
||||
const handleViewRecords = (id: string) => {
|
||||
router.push(`/home/personnel/maintenance/records/${id}`)
|
||||
}
|
||||
|
||||
// 新增维修人员
|
||||
const handleAddMaintenance = () => {
|
||||
router.push('/home/personnel/maintenance/add')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.maintenance-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
background: #42b983;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: #359e75;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.maintenance-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.maintenance-table th,
|
||||
.maintenance-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.maintenance-table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #4e5969;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.maintenance-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-tag.active {
|
||||
background-color: #e6f7ee;
|
||||
color: #00875a;
|
||||
}
|
||||
|
||||
.status-tag.disabled {
|
||||
background-color: #f5f5f5;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.operation-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.operation-buttons button {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.operation-buttons button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-view {
|
||||
background-color: #f6f7ff;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.btn-enable {
|
||||
background-color: #e6f7ee;
|
||||
color: #00875a;
|
||||
}
|
||||
|
||||
.btn-disable {
|
||||
background-color: #ffebe6;
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-top: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,7 @@
|
||||
<!-- src/views/workorder/WorkOrderView.vue -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>工单管理</h1>
|
||||
<p>请选择左侧子菜单查看具体工单信息</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -1,12 +1,25 @@
|
||||
// main.js
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import '@/router/permission'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
// 挂载前初始化
|
||||
app.mount('#app')
|
||||
|
||||
// 全局属性(可选)
|
||||
app.config.globalProperties.$auth = {
|
||||
isAuthenticated: () => {
|
||||
const token = localStorage.getItem('token')
|
||||
return !!token
|
||||
},
|
||||
getUserType: () => localStorage.getItem('userType')
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
// src/router/permission.js
|
||||
import router from './index'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// 需要登录的白名单
|
||||
const whiteList = ['/', '/repairer-register']
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log('路由守卫执行:', to.path)
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 如果目标路由在白名单中,直接放行
|
||||
if (whiteList.includes(to.path)) {
|
||||
return next()
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
if (!authStore.isAuthenticated) {
|
||||
console.warn('未登录,跳转到登录页')
|
||||
next('/')
|
||||
} else {
|
||||
// 已登录,验证用户权限(如果需要)
|
||||
const userType = authStore.getUserType
|
||||
console.log('当前用户类型:', userType)
|
||||
|
||||
// 这里可以添加权限验证逻辑
|
||||
// 例如:只有特定用户类型可以访问某些页面
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
// 路由守卫后置钩子
|
||||
router.afterEach((to, from) => {
|
||||
console.log('路由切换完成:', from.path, '->', to.path)
|
||||
})
|
||||
@ -1,11 +1,47 @@
|
||||
// src/services/api.js
|
||||
import axios from 'axios'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
// 创建最基本的axios实例
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://localhost:8080', // Adjust to your backend URL
|
||||
baseURL: 'http://localhost:8080',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log(`请求: ${config.method.toUpperCase()} ${config.url}`)
|
||||
|
||||
// 添加认证头
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
// 处理401未授权错误
|
||||
if (error.response?.status === 401) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.logout()
|
||||
window.location.href = '/'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default apiClient
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
// src/stores/auth.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import router from '@/router/index.js'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
// 状态
|
||||
const user = ref(null)
|
||||
const token = ref(localStorage.getItem('token'))
|
||||
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||
|
||||
// 登录
|
||||
const login = (userData, authToken) => {
|
||||
user.value = userData
|
||||
token.value = authToken
|
||||
|
||||
// 存储到本地
|
||||
localStorage.setItem('token', authToken)
|
||||
localStorage.setItem('user', JSON.stringify(userData))
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = () => {
|
||||
if (!user.value) {
|
||||
const storedUser = localStorage.getItem('user')
|
||||
if (storedUser) {
|
||||
user.value = JSON.parse(storedUser)
|
||||
}
|
||||
}
|
||||
return user.value
|
||||
}
|
||||
|
||||
// 获取用户类型
|
||||
const getUserType = computed(() => {
|
||||
return user.value?.userType || localStorage.getItem('userType')
|
||||
})
|
||||
|
||||
// 获取维修人员ID
|
||||
const getRepairmanId = computed(() => {
|
||||
return user.value?.repairmanId || localStorage.getItem('repairmanId')
|
||||
})
|
||||
|
||||
// 登出
|
||||
const logout = () => {
|
||||
user.value = null
|
||||
token.value = null
|
||||
|
||||
// 清除本地存储
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('userType')
|
||||
localStorage.removeItem('repairmanId')
|
||||
localStorage.removeItem('userId')
|
||||
localStorage.removeItem('username')
|
||||
|
||||
// 跳转到登录页
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 初始化时从本地存储恢复
|
||||
const initFromStorage = () => {
|
||||
const storedToken = localStorage.getItem('token')
|
||||
const storedUser = localStorage.getItem('user')
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
token.value = storedToken
|
||||
user.value = JSON.parse(storedUser)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
isAuthenticated,
|
||||
getUserType,
|
||||
getRepairmanId,
|
||||
login,
|
||||
logout,
|
||||
getUserInfo,
|
||||
initFromStorage
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,33 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useDeviceStore = defineStore('device', () => {
|
||||
const waterSuppliers = ref([
|
||||
{
|
||||
id: 'A106',
|
||||
name: '供水机 #A106',
|
||||
location: 'A区教学楼 1楼走廊',
|
||||
status: '正常运行',
|
||||
waterLevel: 66,
|
||||
storage: 360,
|
||||
floatValves: {
|
||||
high: { status: '开启', threshold: 90 },
|
||||
low: { status: '关闭', threshold: 10 }
|
||||
},
|
||||
leakDetection: {
|
||||
status: '无漏水',
|
||||
lastChecked: '2024-12-26 10:30'
|
||||
}
|
||||
},
|
||||
// ... 更多设备数据
|
||||
])
|
||||
|
||||
const getWaterSupplierById = (id) => {
|
||||
return waterSuppliers.value.find(device => device.id === id || device.name.includes(id))
|
||||
}
|
||||
|
||||
return {
|
||||
waterSuppliers,
|
||||
getWaterSupplierById
|
||||
}
|
||||
})
|
||||
@ -0,0 +1,534 @@
|
||||
<template>
|
||||
<div class="inspection-form">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span class="back-btn" @click="goBack">返回</span>
|
||||
</div>
|
||||
<div class="header-title">巡检表单</div>
|
||||
<div class="header-right"></div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content">
|
||||
<div class="form-container">
|
||||
<!-- 设备状态 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">设备状态</div>
|
||||
<div class="status-selector">
|
||||
<div
|
||||
class="status-option"
|
||||
:class="{ 'active': selectedStatus === 'normal' }"
|
||||
@click="selectStatus('normal')"
|
||||
>
|
||||
正常
|
||||
</div>
|
||||
<div
|
||||
class="status-option"
|
||||
:class="{ 'active': selectedStatus === 'warning' }"
|
||||
@click="selectStatus('warning')"
|
||||
>
|
||||
异常
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 异常描述 -->
|
||||
<div v-if="selectedStatus === 'warning'" class="form-section">
|
||||
<div class="section-title">异常描述</div>
|
||||
<textarea
|
||||
v-model="abnormalDescription"
|
||||
class="abnormal-textarea"
|
||||
placeholder="请描述处理异常..."
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 现场照片 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">现场照片</div>
|
||||
<div class="photo-upload">
|
||||
<div class="upload-area" @click="uploadPhoto">
|
||||
<div class="upload-icon">📷</div>
|
||||
<div class="upload-text">点击上传照片</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
@change="handleFileUpload"
|
||||
accept="image/*"
|
||||
style="display: none"
|
||||
/>
|
||||
|
||||
<!-- 照片预览 -->
|
||||
<div v-if="uploadedPhotos.length > 0" class="photo-preview">
|
||||
<div v-for="(photo, index) in uploadedPhotos" :key="index" class="preview-item">
|
||||
<div class="preview-image">
|
||||
<img :src="photo.url" :alt="`照片${index + 1}`" />
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<button class="preview-btn delete" @click="removePhoto(index)">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="submit-btn" @click="submitInspection" :disabled="submitting">
|
||||
{{ submitting ? '提交中...' : '提交巡检' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 提交成功弹窗 -->
|
||||
<div v-if="showSuccessModal" class="success-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-icon">✅</div>
|
||||
<div class="modal-title">提交成功</div>
|
||||
<button class="modal-btn" @click="closeSuccessModal">
|
||||
返回
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<div class="bottom-nav">
|
||||
<div class="nav-item" @click="goToHome">首页</div>
|
||||
<div class="nav-item" @click="goToInspection">巡检</div>
|
||||
<div class="nav-item" @click="goToWorkOrders">工单</div>
|
||||
<div class="nav-item" @click="goToProfile">我的</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 设备状态
|
||||
const selectedStatus = ref('normal')
|
||||
const abnormalDescription = ref('')
|
||||
|
||||
// 照片上传
|
||||
const uploadedPhotos = ref([])
|
||||
const fileInput = ref(null)
|
||||
|
||||
// 提交状态
|
||||
const submitting = ref(false)
|
||||
const showSuccessModal = ref(false)
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const goToHome = () => {
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
const goToInspection = () => {
|
||||
router.push('/inspection')
|
||||
}
|
||||
|
||||
const goToWorkOrders = () => {
|
||||
router.push('/work-orders')
|
||||
}
|
||||
|
||||
const goToProfile = () => {
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
// 选择设备状态
|
||||
const selectStatus = (status) => {
|
||||
selectedStatus.value = status
|
||||
if (status === 'normal') {
|
||||
abnormalDescription.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 上传照片
|
||||
const uploadPhoto = () => {
|
||||
fileInput.value.click()
|
||||
}
|
||||
|
||||
const handleFileUpload = (event) => {
|
||||
const file = event.target.files[0]
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
uploadedPhotos.value.push({
|
||||
file: file,
|
||||
url: e.target.result,
|
||||
name: file.name
|
||||
})
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
// 重置文件输入
|
||||
event.target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const removePhoto = (index) => {
|
||||
uploadedPhotos.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 提交巡检
|
||||
const submitInspection = async () => {
|
||||
if (selectedStatus.value === 'warning' && !abnormalDescription.value.trim()) {
|
||||
alert('请填写异常描述')
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
|
||||
// 模拟API调用
|
||||
try {
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
|
||||
console.log('提交巡检数据:', {
|
||||
status: selectedStatus.value,
|
||||
description: abnormalDescription.value,
|
||||
photos: uploadedPhotos.value.length
|
||||
})
|
||||
|
||||
// 显示成功弹窗
|
||||
showSuccessModal.value = true
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
alert('提交失败,请重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭成功弹窗
|
||||
const closeSuccessModal = () => {
|
||||
showSuccessModal.value = false
|
||||
// 返回巡检页面
|
||||
router.push('/inspection')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时可以选择默认设备
|
||||
console.log('巡检表单页面加载')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inspection-form {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f8f9fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.header {
|
||||
background: white;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: #096dd9;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 表单区块 */
|
||||
.form-section {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 状态选择器 */
|
||||
.status-selector {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-option {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.status-option:hover {
|
||||
border-color: #1890ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-option.active {
|
||||
border-color: #1890ff;
|
||||
background: #f0f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 异常描述文本框 */
|
||||
.abnormal-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.abnormal-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
|
||||
}
|
||||
|
||||
.abnormal-textarea::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 照片上传区域 */
|
||||
.photo-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
width: 100%;
|
||||
padding: 40px 20px;
|
||||
border: 2px dashed #e0e0e0;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.upload-area:hover {
|
||||
border-color: #1890ff;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 照片预览 */
|
||||
.photo-preview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.preview-btn.delete {
|
||||
background: #ff4d4f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.preview-btn.delete:hover {
|
||||
background: #ff7875;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 20px;
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 成功弹窗 */
|
||||
.success-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
width: 280px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.modal-btn:hover {
|
||||
background: #096dd9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 底部导航栏 */
|
||||
.bottom-nav {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 8px 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="inspection-page">
|
||||
<!-- 顶部标题栏 -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span class="back-btn" @click="goBack">返回</span>
|
||||
</div>
|
||||
<div class="header-title">设备巡检</div>
|
||||
<div class="header-right">
|
||||
<span class="my-area-btn">我的片区</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="main-content">
|
||||
<!-- 片区列表 -->
|
||||
<div class="area-list">
|
||||
<!-- 岳麓片区 -->
|
||||
<div class="area-card">
|
||||
<div class="area-info">
|
||||
<div class="area-name">岳麓片区</div>
|
||||
<div class="area-details">5所学校 · 12台制水机</div>
|
||||
</div>
|
||||
<div class="area-divider"></div>
|
||||
<button class="view-btn" @click="viewYueluArea">查看</button>
|
||||
</div>
|
||||
|
||||
<!-- 雨花片区 -->
|
||||
<div class="area-card">
|
||||
<div class="area-info">
|
||||
<div class="area-name">雨花片区</div>
|
||||
<div class="area-details">3所学校 · 8台制水机</div>
|
||||
</div>
|
||||
<div class="area-divider"></div>
|
||||
<button class="view-btn" @click="viewYuhuaArea">查看</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部导航栏 -->
|
||||
<div class="bottom-nav">
|
||||
<div class="nav-item" @click="goToHome">
|
||||
<span>首页</span>
|
||||
</div>
|
||||
<div class="nav-item active">
|
||||
<span>巡检</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToWorkOrders">
|
||||
<span>工单</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="goToProfile">
|
||||
<span>我的</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const goToHome = () => {
|
||||
router.push('/home')
|
||||
}
|
||||
|
||||
const goToWorkOrders = () => {
|
||||
console.log('跳转到工单页面')
|
||||
router.push('/work-orders')
|
||||
}
|
||||
|
||||
const goToProfile = () => {
|
||||
console.log('跳转到我的页面')
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
const viewYueluArea = () => {
|
||||
console.log('查看岳麓片区详情')
|
||||
// 跳转到扫码页面
|
||||
router.push('/inspection/scan')
|
||||
}
|
||||
|
||||
const viewYuhuaArea = () => {
|
||||
console.log('查看雨花片区详情')
|
||||
// 跳转到扫码页面
|
||||
router.push('/inspection/scan')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.inspection-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f8f9fa;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.header {
|
||||
background: white;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.back-btn,
|
||||
.my-area-btn {
|
||||
font-size: 14px;
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.back-btn:hover,
|
||||
.my-area-btn:hover {
|
||||
color: #096dd9;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.my-area-btn {
|
||||
text-align: right;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 20px 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 片区列表 */
|
||||
.area-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.area-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.area-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.area-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.area-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.area-details {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.area-divider {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
background: #e8e8e8;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 8px 24px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
background: #096dd9;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 底部导航栏 */
|
||||
.bottom-nav {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue