feat: complete Flutter to UniApp rewrite
1
.kiro/specs/flutter-to-uniapp-rewrite/.config.kiro
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"specId": "8912b026-4bcf-48e3-ad23-2f417cfc5c2b", "workflowType": "requirements-first", "specType": "feature"}
|
||||
734
.kiro/specs/flutter-to-uniapp-rewrite/design.md
Normal file
|
|
@ -0,0 +1,734 @@
|
|||
# 技术设计文档:绥时录 — Flutter 到 UniApp 重写
|
||||
|
||||
## 概述
|
||||
|
||||
本设计文档描述将"绥时录"ODF 端口管理系统从 Flutter 重写为 UniApp (Vue 3) 的技术方案。应用管理公司 → 地区 → 机房 → 机架(ODF) → 端口的层级数据,支持端口状态查看、备注编辑、搜索、历史故障记录等功能。
|
||||
|
||||
UniApp 项目基于 Vue 3 Composition API,使用条件编译兼容多端(App、H5、小程序),目标目录为 `odf-uniapp/`。
|
||||
|
||||
### 关键设计决策
|
||||
|
||||
1. **Vue 3 Composition API**:manifest.json 已配置 `"vueVersion": "3"`,使用 `<script setup>` 语法简化代码
|
||||
2. **无 Vuex/Pinia**:应用状态简单,使用 `reactive` 对象 + 模块导出实现轻量级全局状态管理
|
||||
3. **uni.request 封装**:不引入 axios,直接封装 `uni.request` 实现 API 通信层
|
||||
4. **图片资源**:从 Flutter 项目 `app/assets/images/` 直接复制到 `odf-uniapp/static/images/`
|
||||
5. **弹窗组件**:使用 `uni.navigateTo` + 页面方式或 `<uni-popup>` 组件实现,Port_Edit_Dialog 和 Add_Note_Dialog 作为子组件嵌入页面
|
||||
|
||||
## 架构
|
||||
|
||||
### 整体架构图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph 页面层 Pages
|
||||
StartPage[启动页]
|
||||
LoginPage[登录页]
|
||||
CompanyList[公司列表/首页]
|
||||
RegionList[地区列表]
|
||||
RoomList[机房列表]
|
||||
RackList[机架列表]
|
||||
RackDetail[机架详情]
|
||||
SearchPage[搜索页]
|
||||
SettingsPage[设置页]
|
||||
ChangePassword[修改密码页]
|
||||
end
|
||||
|
||||
subgraph 组件层 Components
|
||||
PortEditDialog[端口编辑弹窗]
|
||||
AddNoteDialog[添加备注弹窗]
|
||||
UpdateDialog[版本更新弹窗]
|
||||
NavBar[自定义导航栏]
|
||||
StatusDot[状态圆点]
|
||||
ListCard[列表卡片]
|
||||
end
|
||||
|
||||
subgraph 服务层 Services
|
||||
API[api.js - HTTP 请求封装]
|
||||
Auth[auth.js - 认证管理]
|
||||
Storage[storage.js - 本地存储]
|
||||
end
|
||||
|
||||
subgraph 状态层 Store
|
||||
AppStore[store/index.js - 全局状态]
|
||||
end
|
||||
|
||||
页面层 --> 组件层
|
||||
页面层 --> 服务层
|
||||
页面层 --> 状态层
|
||||
组件层 --> 服务层
|
||||
服务层 --> Storage
|
||||
```
|
||||
|
||||
### 导航流程图
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Start[启动页] -->|Token有效| Home[公司列表]
|
||||
Start -->|Token无效/401| Login[登录页]
|
||||
Login -->|登录成功| Home
|
||||
Home --> Region[地区列表]
|
||||
Home --> Search[搜索页]
|
||||
Home --> Settings[设置页]
|
||||
Region --> Room[机房列表]
|
||||
Room --> Rack[机架列表]
|
||||
Rack --> Detail[机架详情]
|
||||
Detail -->|点击端口| PortEdit[端口编辑弹窗]
|
||||
PortEdit -->|添加备注| AddNote[添加备注弹窗]
|
||||
Settings --> ChangePwd[修改密码]
|
||||
Settings -->|退出登录| Login
|
||||
Search -->|点击机房| Rack
|
||||
Search -->|点击端口| Detail
|
||||
```
|
||||
|
||||
## 组件与接口
|
||||
|
||||
### UniApp 项目目录结构
|
||||
|
||||
```
|
||||
odf-uniapp/
|
||||
├── pages/
|
||||
│ ├── start/index.vue # 启动页
|
||||
│ ├── login/index.vue # 登录页
|
||||
│ ├── home/index.vue # 首页(公司列表)
|
||||
│ ├── region/index.vue # 地区列表
|
||||
│ ├── room/index.vue # 机房列表
|
||||
│ ├── rack/index.vue # 机架列表
|
||||
│ ├── rack-detail/index.vue # 机架详情
|
||||
│ ├── search/index.vue # 搜索页
|
||||
│ ├── settings/index.vue # 设置页
|
||||
│ └── change-password/index.vue # 修改密码页
|
||||
├── components/
|
||||
│ ├── port-edit-dialog.vue # 端口编辑弹窗
|
||||
│ ├── add-note-dialog.vue # 添加备注弹窗
|
||||
│ └── update-dialog.vue # 版本更新弹窗
|
||||
├── services/
|
||||
│ ├── api.js # HTTP 请求封装
|
||||
│ ├── auth.js # 认证相关 API
|
||||
│ ├── home.js # 首页相关 API
|
||||
│ ├── machine.js # 机房/机架/端口 API
|
||||
│ └── search.js # 搜索 API
|
||||
├── store/
|
||||
│ └── index.js # 全局状态管理
|
||||
├── static/
|
||||
│ └── images/ # 图片资源(从 Flutter 复制)
|
||||
│ ├── home_bg.png
|
||||
│ ├── login_bg.png
|
||||
│ ├── ic_back.png
|
||||
│ ├── ic_refresh.png
|
||||
│ ├── ic_search.png
|
||||
│ ├── ic_set.png
|
||||
│ ├── ic_exit.png
|
||||
│ └── ic_update.png
|
||||
├── App.vue
|
||||
├── main.js
|
||||
├── pages.json # 路由配置
|
||||
├── manifest.json
|
||||
├── uni.scss # 全局样式变量
|
||||
└── index.html
|
||||
```
|
||||
|
||||
|
||||
### 路由配置 (pages.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/start/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/home/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "", "enablePullDownRefresh": true }
|
||||
},
|
||||
{
|
||||
"path": "pages/region/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/room/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "", "enablePullDownRefresh": true }
|
||||
},
|
||||
{
|
||||
"path": "pages/rack/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "", "enablePullDownRefresh": true }
|
||||
},
|
||||
{
|
||||
"path": "pages/rack-detail/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/search/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/settings/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/change-password/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "绥时录",
|
||||
"navigationBarBackgroundColor": "#1A73EC",
|
||||
"backgroundColor": "#F5F5F5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
说明:所有页面使用 `"navigationStyle": "custom"` 自定义导航栏,以匹配 Flutter 原版的 UI 风格(自定义返回按钮、标题、右侧操作按钮)。启动页为 pages 数组第一项,应用启动时自动加载。
|
||||
|
||||
### API 通信层设计 (services/api.js)
|
||||
|
||||
```javascript
|
||||
// services/api.js
|
||||
import store from '@/store'
|
||||
|
||||
const BASE_URL = 'http://49.233.115.141:11082'
|
||||
const TIMEOUT = 20000
|
||||
|
||||
/**
|
||||
* 统一请求封装
|
||||
* @param {string} method - GET | POST
|
||||
* @param {string} url - 接口路径
|
||||
* @param {object} data - 请求参数
|
||||
* @returns {Promise<{code, msg, data}>}
|
||||
*/
|
||||
export function request(method, url, data = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${store.token}`,
|
||||
'Userid': store.userId,
|
||||
'Username': store.userName
|
||||
}
|
||||
uni.request({
|
||||
url: BASE_URL + url,
|
||||
method,
|
||||
data: method === 'GET' ? undefined : data,
|
||||
// GET 参数通过 data 字段传递,UniApp 会自动拼接为 queryString
|
||||
...(method === 'GET' ? { data } : {}),
|
||||
header,
|
||||
timeout: TIMEOUT,
|
||||
success(res) {
|
||||
const { code, msg, data: resData } = res.data
|
||||
resolve({ code, msg, data: resData })
|
||||
},
|
||||
fail(err) {
|
||||
reject({ code: -1, msg: err.errMsg || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const get = (url, params) => request('GET', url, params)
|
||||
export const post = (url, data) => request('POST', url, data)
|
||||
```
|
||||
|
||||
### API 接口模块
|
||||
|
||||
```javascript
|
||||
// services/auth.js
|
||||
import { get, post } from './api'
|
||||
|
||||
export const appLogin = (username, password) => post('/appLogin', { username, password })
|
||||
export const checkPermission = () => get('/business/OdfPorts/odf')
|
||||
export const updatePassword = (oldPassword, newPassword) =>
|
||||
post('/system/user/profile/updateUserPwd', { oldPassword, newPassword })
|
||||
```
|
||||
|
||||
```javascript
|
||||
// services/home.js
|
||||
import { get } from './api'
|
||||
|
||||
export const getCompanyList = () => get('/business/OdfRooms/getcompany')
|
||||
export const getDictUnitType = () => get('/system/dict/data/type/odf_ports_unit_type')
|
||||
export const getDictBusinessType = () => get('/system/dict/data/type/odf_ports_business_type')
|
||||
export const checkAppVersion = (version) => get('/webapi/CheckAppVersion', { version })
|
||||
```
|
||||
|
||||
```javascript
|
||||
// services/machine.js
|
||||
import { get, post } from './api'
|
||||
|
||||
export const getRegionList = (deptId) => get('/business/OdfRooms/getregion', { deptId })
|
||||
export const getRoomList = (pageNum, pageSize, deptId) =>
|
||||
get('/business/OdfRooms/list', { pageNum, pageSize, deptId })
|
||||
export const getRackList = (pageNum, pageSize, roomId) =>
|
||||
get('/business/OdfRacks/list', { pageNum, pageSize, roomId })
|
||||
export const getRackDetail = (RackId) => get('/business/OdfPorts/mlist', { RackId })
|
||||
export const getPortDetail = (id) => get(`/business/OdfPorts/${id}`)
|
||||
export const savePort = (data) => post('/business/OdfPorts/save', data)
|
||||
```
|
||||
|
||||
```javascript
|
||||
// services/search.js
|
||||
import { get } from './api'
|
||||
|
||||
export const searchPorts = (key, pageNum, pageSize) =>
|
||||
get('/business/OdfPorts/search2', { key, pageNum, pageSize })
|
||||
```
|
||||
|
||||
### 全局状态管理 (store/index.js)
|
||||
|
||||
使用 Vue 3 `reactive` 实现轻量级状态管理,无需引入 Vuex/Pinia:
|
||||
|
||||
```javascript
|
||||
// store/index.js
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const store = reactive({
|
||||
// 认证信息
|
||||
token: uni.getStorageSync('token') || '',
|
||||
userId: uni.getStorageSync('userId') || '',
|
||||
userName: uni.getStorageSync('userName') || '',
|
||||
isPermission: false,
|
||||
|
||||
// 字典数据
|
||||
dictUnitTypes: [], // 设备型号列表
|
||||
dictBusinessTypes: [], // 业务类型列表
|
||||
|
||||
// 设置认证信息
|
||||
setAuth(token, userId, userName) {
|
||||
this.token = token
|
||||
this.userId = userId
|
||||
this.userName = userName
|
||||
uni.setStorageSync('token', token)
|
||||
uni.setStorageSync('userId', userId)
|
||||
uni.setStorageSync('userName', userName)
|
||||
},
|
||||
|
||||
// 清除认证信息
|
||||
clearAuth() {
|
||||
this.token = ''
|
||||
this.userId = ''
|
||||
this.userName = ''
|
||||
this.isPermission = false
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userId')
|
||||
uni.removeStorageSync('userName')
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
```
|
||||
|
||||
### 组件设计
|
||||
|
||||
#### 1. 端口编辑弹窗 (port-edit-dialog.vue)
|
||||
|
||||
- Props: `visible` (Boolean), `portId` (String/Number)
|
||||
- Events: `@close`, `@saved`
|
||||
- 行为:打开时调用 `getPortDetail(portId)` 加载数据,根据 `store.isPermission` 控制编辑/只读模式
|
||||
- 内部状态:`remarks`, `opticalAttenuation`, `opticalCableOffRemarks`, `historyFault[]`, `status`
|
||||
- 子组件交互:点击"添加备注"按钮显示 AddNoteDialog,回调将格式化文本写入 remarks
|
||||
|
||||
#### 2. 添加备注弹窗 (add-note-dialog.vue)
|
||||
|
||||
- Props: `visible` (Boolean)
|
||||
- Events: `@close`, `@confirm(formattedText)`
|
||||
- 内部状态:`businessName`, `selectedUnitType`, `selectedBusinessType`, `portNumber1/2/3`
|
||||
- 格式化输出:`"{业务名称} {设备型号} {业务类型} {端口号1}/{端口号2}/{端口号3}"`
|
||||
- 下拉选项来源:`store.dictUnitTypes` 和 `store.dictBusinessTypes`
|
||||
|
||||
#### 3. 版本更新弹窗 (update-dialog.vue)
|
||||
|
||||
- Props: `visible` (Boolean), `downloadUrl` (String)
|
||||
- 行为:模态弹窗,不可通过返回键或点击空白关闭
|
||||
- 点击"去更新"调用 `plus.runtime.openURL(downloadUrl)` 或 `window.open(downloadUrl)`
|
||||
|
||||
## 数据模型
|
||||
|
||||
基于 Flutter 源码中的 Bean 类,定义 UniApp 中使用的 JavaScript 数据结构:
|
||||
|
||||
### 认证相关
|
||||
|
||||
```javascript
|
||||
// LoginResult
|
||||
{
|
||||
jwt: String, // JWT Token
|
||||
userId: Number, // 用户 ID
|
||||
userName: String // 用户名
|
||||
}
|
||||
```
|
||||
|
||||
### 公司/地区
|
||||
|
||||
```javascript
|
||||
// Company / Region(共用结构)
|
||||
{
|
||||
deptId: Number, // 部门/地区 ID
|
||||
deptName: String // 部门/地区名称
|
||||
}
|
||||
```
|
||||
|
||||
### 机房
|
||||
|
||||
```javascript
|
||||
// Room(分页列表项)
|
||||
{
|
||||
id: Number,
|
||||
deptId: Number,
|
||||
deptName: String,
|
||||
roomName: String,
|
||||
roomAddress: String,
|
||||
racksCount: Number, // ODF 机架数量
|
||||
remarks: String,
|
||||
createdAt: String,
|
||||
updatedAt: String
|
||||
}
|
||||
|
||||
// RoomPageResult(分页响应)
|
||||
{
|
||||
pageSize: Number,
|
||||
pageIndex: Number,
|
||||
totalNum: Number,
|
||||
totalPage: Number,
|
||||
result: Room[]
|
||||
}
|
||||
```
|
||||
|
||||
### 机架
|
||||
|
||||
```javascript
|
||||
// Rack(分页列表项)
|
||||
{
|
||||
id: Number,
|
||||
roomId: Number,
|
||||
sequenceNumber: Number,
|
||||
rackName: String,
|
||||
frameCount: Number,
|
||||
createdAt: String,
|
||||
updatedAt: String
|
||||
}
|
||||
|
||||
// RackPageResult(分页响应)
|
||||
{
|
||||
pageSize: Number,
|
||||
pageIndex: Number,
|
||||
totalNum: Number,
|
||||
totalPage: Number,
|
||||
result: Rack[]
|
||||
}
|
||||
```
|
||||
|
||||
### 机架详情(端口矩阵)
|
||||
|
||||
```javascript
|
||||
// Frame(机架详情接口返回的 Frame 分组)
|
||||
{
|
||||
id: Number,
|
||||
name: String, // Frame 名称
|
||||
odfPortsList: PortRow[]
|
||||
}
|
||||
|
||||
// PortRow(端口行,每行包含多个端口)
|
||||
{
|
||||
name: String, // 行名称
|
||||
rowList: PortItem[]
|
||||
}
|
||||
|
||||
// PortItem(单个端口)
|
||||
{
|
||||
id: Number,
|
||||
name: String, // 端口名称
|
||||
status: Number, // 0=已断开, 1=已连接
|
||||
tips: String // 端口提示文字(显示在圆形内)
|
||||
}
|
||||
```
|
||||
|
||||
### 端口详情
|
||||
|
||||
```javascript
|
||||
// PortDetail(端口编辑弹窗使用)
|
||||
{
|
||||
id: Number,
|
||||
name: String,
|
||||
roomId: Number,
|
||||
roomName: String,
|
||||
rackId: Number,
|
||||
rackName: String,
|
||||
frameId: Number,
|
||||
frameName: String,
|
||||
deptId: Number,
|
||||
rowNumber: Number,
|
||||
portNumber: Number,
|
||||
status: Number, // 0=已断开, 1=已连接
|
||||
remarks: String, // 备注说明
|
||||
opticalAttenuation: String, // 光衰信息
|
||||
opticalCableOffRemarks: String, // 光缆段信息
|
||||
historyRemarks: String, // 历史障碍原因(旧字段)
|
||||
historyFault: HistoryFault[], // 历史障碍记录列表
|
||||
createdAt: String,
|
||||
updatedAt: String,
|
||||
statusLabel: String,
|
||||
deptName: String,
|
||||
equipmentModel: String,
|
||||
businessType: String
|
||||
}
|
||||
|
||||
// HistoryFault
|
||||
{
|
||||
faultTime: String, // 障碍发生时间 "yyyy-MM-dd HH:mm:ss"
|
||||
faultReason: String // 障碍原因
|
||||
}
|
||||
```
|
||||
|
||||
### 搜索结果
|
||||
|
||||
```javascript
|
||||
// SearchResult(搜索接口返回)
|
||||
{
|
||||
rooms: SearchRoom[], // 匹配的机房列表
|
||||
ports: PortPageResult // 匹配的端口分页数据
|
||||
}
|
||||
|
||||
// SearchRoom
|
||||
{
|
||||
roomId: Number,
|
||||
roomName: String,
|
||||
roomAddress: String,
|
||||
remarks: String,
|
||||
deptName: String
|
||||
}
|
||||
|
||||
// SearchPort(端口搜索结果项)
|
||||
{
|
||||
id: Number,
|
||||
name: String,
|
||||
roomId: Number,
|
||||
roomName: String,
|
||||
rackId: Number,
|
||||
rackName: String,
|
||||
frameId: Number,
|
||||
frameName: String,
|
||||
rowNumber: Number,
|
||||
portNumber: Number,
|
||||
status: Number,
|
||||
remarks: String,
|
||||
opticalAttenuation: String,
|
||||
opticalCableOffRemarks: String,
|
||||
historyRemarks: String,
|
||||
address: String
|
||||
}
|
||||
|
||||
// PortPageResult
|
||||
{
|
||||
pageSize: Number,
|
||||
pageIndex: Number,
|
||||
totalNum: Number,
|
||||
totalPage: Number,
|
||||
result: SearchPort[]
|
||||
}
|
||||
```
|
||||
|
||||
### 字典数据
|
||||
|
||||
```javascript
|
||||
// DictItem
|
||||
{
|
||||
dictCode: Number,
|
||||
dictSort: Number,
|
||||
dictLabel: String,
|
||||
dictValue: String
|
||||
}
|
||||
```
|
||||
|
||||
### 版本更新
|
||||
|
||||
```javascript
|
||||
// UpdateInfo
|
||||
{
|
||||
needUpdate: Boolean,
|
||||
forceUpdate: Boolean,
|
||||
latestVersion: String,
|
||||
downloadUrl: String,
|
||||
updateDescription: String
|
||||
}
|
||||
```
|
||||
|
||||
### 端口保存请求
|
||||
|
||||
```javascript
|
||||
// PortSaveRequest(提交到 POST /business/OdfPorts/save)
|
||||
{
|
||||
Id: Number,
|
||||
Status: Number, // 0 或 1
|
||||
Remarks: String,
|
||||
OpticalAttenuation: String,
|
||||
HistoryRemarks: String,
|
||||
HistoryFault: HistoryFault[],
|
||||
OpticalCableOffRemarks: String
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 正确性属性 (Correctness Properties)
|
||||
|
||||
*属性(Property)是指在系统所有有效执行中都应保持为真的特征或行为——本质上是对系统应做什么的形式化陈述。属性是人类可读规范与机器可验证正确性保证之间的桥梁。*
|
||||
|
||||
### Property 1: 认证数据持久化往返 (Auth Data Persistence Round-Trip)
|
||||
|
||||
*For any* 有效的认证数据(token、userId、userName),调用 `store.setAuth(token, userId, userName)` 存储后,从 `uni.getStorageSync` 读取的值应与原始输入完全一致。
|
||||
|
||||
**Validates: Requirements 1.1, 2.3**
|
||||
|
||||
### Property 2: 登录请求包含凭证
|
||||
|
||||
*For any* 非空的用户名和密码字符串对,调用登录接口时发送的请求体应同时包含 `username` 和 `password` 字段,且值与输入一致。
|
||||
|
||||
**Validates: Requirements 2.2**
|
||||
|
||||
### Property 3: 分页状态管理
|
||||
|
||||
*For any* 分页列表(机房、机架、搜索结果),当前页码为 N 且存在下一页时,触发加载更多操作后,请求的 pageNum 应为 N+1,且新数据应追加到现有列表末尾而非替换,列表总长度应等于之前长度加上新返回的数据条数。
|
||||
|
||||
**Validates: Requirements 6.5, 7.5, 11.9**
|
||||
|
||||
### Property 4: 端口状态颜色映射
|
||||
|
||||
*For any* 端口对象,当 `status === 1` 时应映射为绿色(已连接),当 `status === 0` 时应映射为红色(已断开),不存在其他映射结果。
|
||||
|
||||
**Validates: Requirements 8.6, 14.3**
|
||||
|
||||
### Property 5: 权限控制 UI 状态
|
||||
|
||||
*For any* 布尔值 `isPermission`,当 `isPermission` 为 true 时,端口编辑弹窗应显示编辑控件(状态切换按钮、提交按钮、添加备注按钮)且输入框可编辑;当 `isPermission` 为 false 时,所有输入框应禁用且仅显示"关闭"按钮。
|
||||
|
||||
**Validates: Requirements 9.11, 9.12, 9.13**
|
||||
|
||||
### Property 6: 历史障碍记录时间校验
|
||||
|
||||
*For any* 历史障碍记录列表,若列表中存在任一条目的 `faultTime` 为空字符串或仅包含空白字符,则提交操作应被阻止并返回校验失败。
|
||||
|
||||
**Validates: Requirements 9.15**
|
||||
|
||||
### Property 7: 备注格式化字符串
|
||||
|
||||
*For any* 有效的业务名称、设备型号、业务类型和三组端口号,格式化输出应严格匹配模式 `"{业务名称} {设备型号} {业务类型} {端口号1}/{端口号2}/{端口号3}"`,各字段之间以空格分隔,端口号之间以斜杠分隔。
|
||||
|
||||
**Validates: Requirements 10.9**
|
||||
|
||||
### Property 8: 添加备注表单校验
|
||||
|
||||
*For any* 业务名称和三组端口号的输入组合,当业务名称为空或任一端口号为空时,提交应被阻止;当所有必填字段均非空时,提交应被允许。
|
||||
|
||||
**Validates: Requirements 10.6**
|
||||
|
||||
### Property 9: API 响应解析
|
||||
|
||||
*For any* 包含 `code`、`msg`、`data` 字段的有效 JSON 响应对象,API 通信层解析后返回的结果应包含这三个字段且值与原始响应一致。
|
||||
|
||||
**Validates: Requirements 13.4**
|
||||
|
||||
### Property 10: API 异常处理
|
||||
|
||||
*For any* 网络请求异常(超时、连接失败等),API 通信层应返回包含 `code: -1` 和非空错误描述 `msg` 的错误对象,不应抛出未捕获异常。
|
||||
|
||||
**Validates: Requirements 13.5**
|
||||
|
||||
### Property 11: API 请求头构造
|
||||
|
||||
*For any* 已认证状态下的 API 请求,请求头应包含 `Authorization`(格式为 `Bearer {token}`)、`Userid` 和 `Username` 三个字段,且值与当前全局状态中存储的认证信息一致。
|
||||
|
||||
**Validates: Requirements 13.2**
|
||||
|
||||
### Property 12: 默认分页大小
|
||||
|
||||
*For any* 分页列表的首次加载请求,`pageSize` 参数应为 20。
|
||||
|
||||
**Validates: Requirements 14.8**
|
||||
|
||||
### Property 13: 退出登录清除认证状态
|
||||
|
||||
*For any* 已登录状态(token 非空),执行退出登录操作后,`store.token`、`store.userId`、`store.userName` 应为空字符串,且 `uni.getStorageSync('token')` 应返回空值。
|
||||
|
||||
**Validates: Requirements 12.4**
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 网络层错误处理
|
||||
|
||||
| 错误场景 | 处理方式 |
|
||||
|---------|---------|
|
||||
| 网络连接失败 | 返回 `{ code: -1, msg: '网络异常' }`,页面显示 Toast 提示 |
|
||||
| 请求超时(20s) | 同上 |
|
||||
| 服务器返回 401 | 清除本地 Token,跳转登录页 |
|
||||
| 服务器返回 403 | 设置 `isPermission = false`,继续正常使用(只读模式) |
|
||||
| 服务器返回其他非 200 | 显示服务器返回的 `msg` 作为 Toast 提示 |
|
||||
|
||||
### 表单校验错误处理
|
||||
|
||||
| 校验场景 | 提示消息 |
|
||||
|---------|---------|
|
||||
| 登录 - 账号/密码为空 | "请输入账号" / "请输入密码" |
|
||||
| 添加备注 - 业务名称为空 | "请输入业务名称" |
|
||||
| 添加备注 - 端口号为空 | "请输入端口号" |
|
||||
| 端口保存 - 障碍时间未选择 | "请选择障碍发生时间!" |
|
||||
| 修改密码 - 旧密码为空 | "请输入旧密码!" |
|
||||
| 修改密码 - 新密码为空 | "请输入新密码!" |
|
||||
|
||||
### 分页加载边界处理
|
||||
|
||||
- 当 `pageIndex >= totalPage` 时,不再触发加载更多请求
|
||||
- 下拉刷新时重置 `pageNum = 1`,清空现有列表后重新加载
|
||||
- 加载中状态下不重复触发请求(防抖)
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 双重测试方法
|
||||
|
||||
本项目采用单元测试 + 属性测试的双重测试策略:
|
||||
|
||||
- **单元测试**:验证具体示例、边界情况和错误条件
|
||||
- **属性测试**:验证跨所有输入的通用属性
|
||||
|
||||
两者互补,单元测试捕获具体 bug,属性测试验证通用正确性。
|
||||
|
||||
### 属性测试库
|
||||
|
||||
使用 [fast-check](https://github.com/dubzzz/fast-check) 作为 JavaScript 属性测试库,配合 Jest 或 Vitest 测试框架。
|
||||
|
||||
### 属性测试配置
|
||||
|
||||
- 每个属性测试最少运行 **100 次迭代**
|
||||
- 每个属性测试必须以注释引用设计文档中的属性编号
|
||||
- 标签格式:**Feature: flutter-to-uniapp-rewrite, Property {number}: {property_text}**
|
||||
- 每个正确性属性由**单个**属性测试实现
|
||||
|
||||
### 测试范围
|
||||
|
||||
#### 属性测试(Property-Based Tests)
|
||||
|
||||
| 属性 | 测试文件 | 说明 |
|
||||
|-----|---------|------|
|
||||
| P1: 认证数据持久化往返 | `tests/store.test.js` | 生成随机 token/userId/userName,验证存储往返 |
|
||||
| P2: 登录请求包含凭证 | `tests/api.test.js` | 生成随机用户名密码,验证请求体 |
|
||||
| P3: 分页状态管理 | `tests/pagination.test.js` | 生成随机页码和数据,验证追加逻辑 |
|
||||
| P4: 端口状态颜色映射 | `tests/utils.test.js` | 生成随机 status 值(0/1),验证颜色映射 |
|
||||
| P5: 权限控制 UI 状态 | `tests/permission.test.js` | 生成随机布尔值,验证 UI 元素可见性 |
|
||||
| P6: 历史障碍记录时间校验 | `tests/validation.test.js` | 生成随机障碍记录列表,验证校验逻辑 |
|
||||
| P7: 备注格式化字符串 | `tests/format.test.js` | 生成随机业务信息,验证格式化输出 |
|
||||
| P8: 添加备注表单校验 | `tests/validation.test.js` | 生成随机表单输入,验证校验规则 |
|
||||
| P9: API 响应解析 | `tests/api.test.js` | 生成随机 JSON 响应,验证解析结果 |
|
||||
| P10: API 异常处理 | `tests/api.test.js` | 模拟各种异常,验证错误对象格式 |
|
||||
| P11: API 请求头构造 | `tests/api.test.js` | 生成随机认证信息,验证请求头 |
|
||||
| P12: 默认分页大小 | `tests/pagination.test.js` | 验证首次加载 pageSize=20 |
|
||||
| P13: 退出登录清除认证状态 | `tests/store.test.js` | 生成随机认证状态,验证清除逻辑 |
|
||||
|
||||
#### 单元测试(Unit Tests)
|
||||
|
||||
| 测试范围 | 测试文件 | 说明 |
|
||||
|---------|---------|------|
|
||||
| 启动页路由分发 | `tests/start.test.js` | 测试 token 存在/不存在/401/403/200 各场景 |
|
||||
| 登录流程 | `tests/login.test.js` | 测试登录成功、失败、错误提示 |
|
||||
| 版本更新检查 | `tests/update.test.js` | 测试 needUpdate=true/false 场景 |
|
||||
| 密码修改校验 | `tests/password.test.js` | 测试空密码、成功、状态码 110 场景 |
|
||||
| 搜索结果分区 | `tests/search.test.js` | 测试机房和端口结果分区展示 |
|
||||
243
.kiro/specs/flutter-to-uniapp-rewrite/requirements.md
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
# 需求文档:绥时录 — Flutter 到 UniApp 重写
|
||||
|
||||
## 简介
|
||||
|
||||
将现有 Flutter 版本的"绥时录"ODF(光纤配线架)端口管理系统完全重写为 UniApp 版本。该系统用于管理公司 → 地区 → 机房 → 机架(ODF) → 端口的层级数据,支持端口状态查看、备注编辑、搜索、历史故障记录等功能。目标项目目录为 `odf-uniapp/`。
|
||||
|
||||
## 术语表
|
||||
|
||||
- **Application**: 绥时录 UniApp 移动端应用程序
|
||||
- **Auth_Module**: 负责用户认证、token 管理和权限校验的模块
|
||||
- **Navigation_Module**: 负责页面路由和导航的模块
|
||||
- **API_Client**: 负责与后端服务器通信的 HTTP 请求模块,基础地址为 `http://49.233.115.141:11082`
|
||||
- **Storage_Module**: 负责本地数据持久化的模块(基于 uni.setStorageSync / uni.getStorageSync)
|
||||
- **Company_List_Page**: 首页,展示公司列表
|
||||
- **Region_List_Page**: 地区列表页,展示指定公司下的地区
|
||||
- **Room_List_Page**: 机房列表页,展示指定地区下的机房
|
||||
- **Rack_List_Page**: 机架列表页,展示指定机房下的 ODF 机架
|
||||
- **Rack_Detail_Page**: 机架详情页,展示端口矩阵和端口状态
|
||||
- **Search_Page**: 搜索页,支持按备注关键词搜索机房和端口
|
||||
- **Settings_Page**: 设置页,提供修改密码和退出登录功能
|
||||
- **Change_Password_Page**: 修改密码页
|
||||
- **Login_Page**: 登录页
|
||||
- **Start_Page**: 启动页,负责 token 检测和路由分发
|
||||
- **Port_Edit_Dialog**: 端口编辑弹窗,用于查看和编辑端口详细信息
|
||||
- **Add_Note_Dialog**: 添加备注弹窗,用于快速生成格式化备注
|
||||
- **Update_Dialog**: 版本更新弹窗,提示用户下载新版本
|
||||
- **ODF**: 光纤配线架(Optical Distribution Frame)
|
||||
- **Port**: ODF 机架上的单个光纤端口
|
||||
- **Frame**: ODF 机架内的分组单元,每个 Frame 包含多行端口
|
||||
- **Token**: JWT 格式的用户认证令牌
|
||||
- **Permission**: 用户对 ODF 端口的编辑权限(isPermission 标志)
|
||||
- **Dict_Data**: 系统字典数据,包括设备型号和业务类型
|
||||
|
||||
## 需求
|
||||
|
||||
### 需求 1:应用启动与认证流程
|
||||
|
||||
**用户故事:** 作为用户,我希望应用启动时自动检测登录状态并跳转到正确页面,以便快速进入工作界面。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Application 启动时, THE Start_Page SHALL 从 Storage_Module 读取本地存储的 Token
|
||||
2. WHILE Token 存在且非空, THE Auth_Module SHALL 调用权限检测接口 `GET /business/OdfPorts/odf` 验证 Token 有效性
|
||||
3. WHEN 权限检测接口返回状态码 200, THE Auth_Module SHALL 设置 Permission 为 true 并将 Navigation_Module 导航至 Company_List_Page
|
||||
4. WHEN 权限检测接口返回状态码 403, THE Auth_Module SHALL 设置 Permission 为 false 并将 Navigation_Module 导航至 Company_List_Page
|
||||
5. WHEN 权限检测接口返回状态码 401, THE Navigation_Module SHALL 导航至 Login_Page
|
||||
6. WHEN Token 不存在或为空, THE Navigation_Module SHALL 直接导航至 Login_Page
|
||||
7. THE Start_Page SHALL 在页面中央显示应用名称"绥时录"作为启动标识
|
||||
|
||||
### 需求 2:用户登录
|
||||
|
||||
**用户故事:** 作为用户,我希望通过账号密码登录系统,以便获取操作权限。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Login_Page SHALL 提供账号输入框(提示文字"请输入账号")和密码输入框(提示文字"请输入密码",密码模式遮蔽显示)
|
||||
2. WHEN 用户点击"登录"按钮, THE Auth_Module SHALL 调用 `POST /appLogin` 接口发送 username 和 password 参数
|
||||
3. WHEN 登录接口返回状态码 200, THE Auth_Module SHALL 从响应数据中提取 jwt、userId、userName,将 Token 存储至 Storage_Module,并将 userId 和 userName 保存到全局状态
|
||||
4. WHEN 登录接口返回状态码 200, THE Auth_Module SHALL 调用权限检测接口确认权限后导航至 Company_List_Page
|
||||
5. IF 登录接口返回非 200 状态码, THEN THE Application SHALL 以 Toast 形式显示服务器返回的错误消息
|
||||
6. THE Login_Page SHALL 使用背景图片覆盖整个页面,主题色为 #1A73EC
|
||||
|
||||
### 需求 3:首页(公司列表)
|
||||
|
||||
**用户故事:** 作为用户,我希望在首页看到所有公司列表,以便选择目标公司进入下级管理。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Company_List_Page 加载时, THE API_Client SHALL 调用 `GET /business/OdfRooms/getcompany` 获取公司列表数据
|
||||
2. THE Company_List_Page SHALL 以卡片列表形式展示每个公司的 deptName
|
||||
3. WHEN 用户点击某个公司卡片, THE Navigation_Module SHALL 携带该公司的 deptId 导航至 Region_List_Page
|
||||
4. THE Company_List_Page SHALL 支持下拉刷新,刷新时重新调用公司列表接口
|
||||
5. THE Company_List_Page SHALL 在顶部栏左侧显示刷新图标、中间显示"公司列表"标题、右侧显示设置图标
|
||||
6. WHEN 用户点击设置图标, THE Navigation_Module SHALL 导航至 Settings_Page
|
||||
7. THE Company_List_Page SHALL 在标题栏下方显示搜索入口栏(提示文字"请输入要搜索的备注内容")
|
||||
8. WHEN 用户点击搜索入口栏, THE Navigation_Module SHALL 导航至 Search_Page
|
||||
9. WHEN Company_List_Page 加载时, THE API_Client SHALL 同时调用设备型号字典接口 `GET /system/dict/data/type/odf_ports_unit_type` 和业务类型字典接口 `GET /system/dict/data/type/odf_ports_business_type`,并将结果存储到全局状态
|
||||
10. WHEN Company_List_Page 加载时, THE Application SHALL 调用版本检查接口 `GET /webapi/CheckAppVersion` 并传入当前应用版本号
|
||||
|
||||
### 需求 4:版本更新检查
|
||||
|
||||
**用户故事:** 作为用户,我希望应用能自动检测新版本并提示更新,以便使用最新功能。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN 版本检查接口返回 needUpdate 为 true, THE Application SHALL 弹出 Update_Dialog
|
||||
2. THE Update_Dialog SHALL 显示更新图标、"有新版本请更新"提示文字和"去更新"按钮
|
||||
3. WHEN 用户点击"去更新"按钮, THE Application SHALL 使用系统浏览器打开 downloadUrl 指定的下载地址
|
||||
4. THE Update_Dialog SHALL 为模态弹窗,用户无法通过点击空白区域或返回键关闭
|
||||
|
||||
### 需求 5:地区列表
|
||||
|
||||
**用户故事:** 作为用户,我希望查看指定公司下的地区列表,以便选择目标地区。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Region_List_Page 加载时, THE API_Client SHALL 调用 `GET /business/OdfRooms/getregion` 并传入 deptId 参数获取地区列表
|
||||
2. THE Region_List_Page SHALL 以卡片列表形式展示每个地区的 deptName
|
||||
3. WHEN 用户点击某个地区卡片, THE Navigation_Module SHALL 携带该地区的 deptId 导航至 Room_List_Page
|
||||
4. THE Region_List_Page SHALL 在顶部显示返回按钮和"地区列表"标题
|
||||
|
||||
### 需求 6:机房列表
|
||||
|
||||
**用户故事:** 作为用户,我希望查看指定地区下的机房列表,以便选择目标机房。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Room_List_Page 加载时, THE API_Client SHALL 调用 `GET /business/OdfRooms/list` 并传入 pageNum=1、pageSize=20、deptId 参数获取机房列表
|
||||
2. THE Room_List_Page SHALL 以卡片形式展示每个机房的 roomName(机房名)、roomAddress(地址)和 racksCount(ODF 数量,格式为"ODF: N台")
|
||||
3. WHEN 用户点击某个机房卡片, THE Navigation_Module SHALL 携带该机房的 id 和 roomName 导航至 Rack_List_Page
|
||||
4. THE Room_List_Page SHALL 支持下拉刷新,刷新时重置 pageNum 为 1 并重新加载数据
|
||||
5. WHEN 用户滚动至列表底部(距底部 100px 以内), THE API_Client SHALL 自动递增 pageNum 并加载下一页数据追加到列表末尾
|
||||
6. THE Room_List_Page SHALL 在顶部显示返回按钮和"机房列表"标题
|
||||
|
||||
### 需求 7:机架列表
|
||||
|
||||
**用户故事:** 作为用户,我希望查看指定机房下的 ODF 机架列表,以便选择目标机架查看端口详情。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Rack_List_Page 加载时, THE API_Client SHALL 调用 `GET /business/OdfRacks/list` 并传入 pageNum=1、pageSize=20、roomId 参数获取机架列表
|
||||
2. THE Rack_List_Page SHALL 以卡片列表形式展示每个机架的 rackName
|
||||
3. WHEN 用户点击某个机架卡片, THE Navigation_Module SHALL 携带该机架的 id、rackName 和 roomName 导航至 Rack_Detail_Page
|
||||
4. THE Rack_List_Page SHALL 支持下拉刷新,刷新时重置 pageNum 为 1 并重新加载数据
|
||||
5. WHEN 用户滚动至列表底部(距底部 100px 以内), THE API_Client SHALL 自动递增 pageNum 并加载下一页数据追加到列表末尾
|
||||
6. THE Rack_List_Page SHALL 在顶部显示返回按钮和"机房详情"标题
|
||||
|
||||
### 需求 8:机架详情(端口矩阵)
|
||||
|
||||
**用户故事:** 作为用户,我希望查看机架内所有端口的状态矩阵,以便快速了解端口连接情况。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Rack_Detail_Page 加载时, THE API_Client SHALL 调用 `GET /business/OdfPorts/mlist` 并传入 RackId 参数获取机架详情数据
|
||||
2. WHILE 数据加载中, THE Application SHALL 显示"loading..."加载提示
|
||||
3. THE Rack_Detail_Page SHALL 在顶部显示返回按钮和"{rackName}详情"标题,标题下方显示 roomName(蓝色 #1A73EC 字体加粗)
|
||||
4. THE Rack_Detail_Page SHALL 在 roomName 下方显示状态图例:绿色圆点标注"已连接"、红色圆点标注"已断开"
|
||||
5. THE Rack_Detail_Page SHALL 按 Frame 分组展示端口数据,每个 Frame 显示为一个白色卡片,卡片顶部显示 Frame 名称
|
||||
6. THE Rack_Detail_Page SHALL 在每个 Frame 卡片内按行排列端口,每个端口显示为圆形图标:status=1 时为绿色(已连接),status=0 时为红色(已断开),圆形内显示 tips 文字,圆形下方显示端口 name
|
||||
7. WHEN 端口行内容超出屏幕宽度, THE Rack_Detail_Page SHALL 支持水平滚动查看
|
||||
8. WHEN 用户点击某个端口圆形图标, THE Application SHALL 弹出 Port_Edit_Dialog 并传入该端口的 id
|
||||
|
||||
### 需求 9:端口编辑弹窗
|
||||
|
||||
**用户故事:** 作为用户,我希望查看和编辑端口的详细信息,以便维护端口数据。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. WHEN Port_Edit_Dialog 打开时, THE API_Client SHALL 调用 `GET /business/OdfPorts/{id}` 获取端口详细信息
|
||||
2. WHILE 数据加载中, THE Application SHALL 显示"loading..."加载提示
|
||||
3. THE Port_Edit_Dialog SHALL 显示端口位置信息(格式为"位置:{frameName}{name}")和当前状态(绿色/红色圆点 + "已连接"/"已断开"文字)
|
||||
4. THE Port_Edit_Dialog SHALL 提供备注说明多行输入框(5行高度,提示文字"请输入备注说明")
|
||||
5. THE Port_Edit_Dialog SHALL 在备注输入框旁显示"添加备注"按钮,点击后弹出 Add_Note_Dialog
|
||||
6. THE Port_Edit_Dialog SHALL 提供光衰信息单行输入框(提示文字"请输入光衰信息")
|
||||
7. THE Port_Edit_Dialog SHALL 提供历史障碍记录区域,显示已有记录列表,每条记录包含时间选择器和故障原因输入框
|
||||
8. THE Port_Edit_Dialog SHALL 提供"添加新记录"链接,点击后在历史障碍列表末尾添加一条空记录
|
||||
9. THE Port_Edit_Dialog SHALL 在每条历史障碍记录右侧显示删除按钮(红色"-"),点击后移除该条记录
|
||||
10. THE Port_Edit_Dialog SHALL 提供光缆段信息单行输入框(提示文字"请输入光缆段信息")
|
||||
11. WHILE Permission 为 true, THE Port_Edit_Dialog SHALL 显示"改变状态"区域,包含"连接"(绿色)和"断开"(红色)两个切换按钮,以及提示文字"断开后只清空备注说明,其他内容不影响"
|
||||
12. WHILE Permission 为 true, THE Port_Edit_Dialog SHALL 显示"取消"和"提交"两个操作按钮
|
||||
13. WHILE Permission 为 false, THE Port_Edit_Dialog SHALL 禁用所有输入框和编辑功能,仅显示"关闭"按钮
|
||||
14. WHEN 用户点击"提交"按钮, THE API_Client SHALL 调用 `POST /business/OdfPorts/save` 发送 Id、Status、Remarks、OpticalAttenuation、HistoryRemarks、HistoryFault、OpticalCableOffRemarks 参数
|
||||
15. IF 历史障碍记录中存在未选择时间的条目, THEN THE Application SHALL 以 Toast 显示"请选择障碍发生时间!"并阻止提交
|
||||
16. WHEN 保存接口返回成功, THE Port_Edit_Dialog SHALL 关闭弹窗并触发父页面刷新机架详情数据
|
||||
|
||||
### 需求 10:添加备注弹窗
|
||||
|
||||
**用户故事:** 作为用户,我希望通过结构化表单快速生成格式化备注,以便提高备注录入效率。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Add_Note_Dialog SHALL 显示"添加备注"标题
|
||||
2. THE Add_Note_Dialog SHALL 提供业务名称输入框(提示文字"请输入业务名称")
|
||||
3. THE Add_Note_Dialog SHALL 提供设备型号下拉选择器,选项来源于全局存储的 Dict_Data(odf_ports_unit_type),默认选中第一项
|
||||
4. THE Add_Note_Dialog SHALL 提供业务类型下拉选择器,选项来源于全局存储的 Dict_Data(odf_ports_business_type),默认选中第一项
|
||||
5. THE Add_Note_Dialog SHALL 提供三组端口号数输入框(提示文字分别为"1号端口数"、"2号端口数"、"3号端口数"),仅允许输入数字
|
||||
6. WHEN 用户点击"提交"按钮, THE Add_Note_Dialog SHALL 校验业务名称和三组端口号均不为空
|
||||
7. IF 业务名称为空, THEN THE Application SHALL 以 Toast 显示"请输入业务名称"
|
||||
8. IF 任一端口号为空, THEN THE Application SHALL 以 Toast 显示"请输入端口号"
|
||||
9. WHEN 校验通过, THE Add_Note_Dialog SHALL 将备注内容格式化为"{业务名称} {设备型号} {业务类型} {端口号1}/{端口号2}/{端口号3}"并回填至 Port_Edit_Dialog 的备注输入框,然后关闭弹窗
|
||||
10. THE Add_Note_Dialog SHALL 提供"取消"按钮,点击后关闭弹窗不做任何操作
|
||||
|
||||
### 需求 11:搜索功能
|
||||
|
||||
**用户故事:** 作为用户,我希望通过备注关键词搜索端口和机房,以便快速定位目标设备。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Search_Page SHALL 在顶部显示返回按钮和"搜索"标题
|
||||
2. THE Search_Page SHALL 提供搜索输入框(提示文字"请输入要搜索的备注内容")和"搜索"按钮
|
||||
3. WHEN 用户点击"搜索"按钮或在键盘上按下搜索键, THE API_Client SHALL 调用 `GET /business/OdfPorts/search2` 并传入 key、pageNum=1、pageSize=20 参数
|
||||
4. THE Search_Page SHALL 将搜索结果分为"机房"和"备注信息"两个区域展示
|
||||
5. THE Search_Page SHALL 在"机房"区域以卡片形式展示匹配的机房列表,每个卡片显示 roomName
|
||||
6. WHEN 用户点击机房卡片, THE Navigation_Module SHALL 携带 roomId 和 roomName 导航至 Rack_List_Page
|
||||
7. THE Search_Page SHALL 在"备注信息"区域以卡片形式展示匹配的端口列表,每个卡片显示 roomName、address、rackName(ODF名称)、frameName+name(点位置)、remarks(备注)、opticalAttenuation(光衰信息)、historyRemarks(历史故障原因及时间)、opticalCableOffRemarks(光缆段信息)和当前状态(圆点+文字)
|
||||
8. WHEN 用户点击端口卡片, THE Navigation_Module SHALL 携带 rackId、rackName、roomName 和端口 id 导航至 Rack_Detail_Page,并自动弹出该端口的 Port_Edit_Dialog
|
||||
9. WHEN 用户滚动至搜索结果底部(距底部 100px 以内), THE API_Client SHALL 自动递增 pageNum 并加载下一页端口数据追加到列表末尾
|
||||
|
||||
### 需求 12:设置与密码修改
|
||||
|
||||
**用户故事:** 作为用户,我希望能修改密码和退出登录,以便管理账户安全。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Settings_Page SHALL 在顶部显示返回按钮和"设置"标题
|
||||
2. THE Settings_Page SHALL 显示"修改密码"选项卡片和"退出登录"选项卡片(红色文字)
|
||||
3. WHEN 用户点击"修改密码"卡片, THE Navigation_Module SHALL 导航至 Change_Password_Page
|
||||
4. WHEN 用户点击"退出登录"卡片, THE Auth_Module SHALL 清除 Storage_Module 中的 Token 并将 Navigation_Module 导航至 Login_Page(清除导航栈)
|
||||
5. THE Change_Password_Page SHALL 在顶部显示返回按钮和"修改密码"标题
|
||||
6. THE Change_Password_Page SHALL 提供旧密码输入框(提示文字"请输入旧密码")和新密码输入框(提示文字"请输入新密码")
|
||||
7. WHEN 用户点击"确认修改"按钮, THE API_Client SHALL 调用 `POST /system/user/profile/updateUserPwd` 发送 oldPassword 和 newPassword 参数
|
||||
8. IF 旧密码输入框为空, THEN THE Application SHALL 以 Toast 显示"请输入旧密码!"
|
||||
9. IF 新密码输入框为空, THEN THE Application SHALL 以 Toast 显示"请输入新密码!"
|
||||
10. WHEN 修改密码接口返回状态码 200, THE Application SHALL 以 Toast 显示"修改成功"并返回上一页
|
||||
11. WHEN 修改密码接口返回状态码 110, THE Application SHALL 以 Toast 显示服务器返回的错误消息
|
||||
|
||||
### 需求 13:API 通信层
|
||||
|
||||
**用户故事:** 作为开发者,我希望有统一的 API 通信层,以便所有接口请求遵循一致的规范。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE API_Client SHALL 以 `http://49.233.115.141:11082` 作为所有请求的基础地址
|
||||
2. THE API_Client SHALL 在每个请求的 Header 中携带 `Authorization: Bearer {token}`、`Userid` 和 `Username` 三个字段
|
||||
3. THE API_Client SHALL 将请求超时时间设置为连接超时 20 秒、发送超时 10 秒、接收超时 20 秒
|
||||
4. THE API_Client SHALL 统一解析响应 JSON 结构,提取 code、msg、data 三个字段
|
||||
5. IF 网络请求发生异常, THEN THE API_Client SHALL 返回包含错误码 -1 和异常描述的错误对象
|
||||
6. THE API_Client SHALL 支持 GET 和 POST 两种请求方法,GET 请求参数通过 queryParameters 传递,POST 请求参数通过 request body 传递
|
||||
|
||||
### 需求 14:全局 UI 规范
|
||||
|
||||
**用户故事:** 作为用户,我希望应用具有统一的视觉风格和交互体验。
|
||||
|
||||
#### 验收标准
|
||||
|
||||
1. THE Application SHALL 使用 #1A73EC 作为主题色,应用于按钮、搜索框、链接等交互元素
|
||||
2. THE Application SHALL 在所有列表页面使用统一的背景图片覆盖
|
||||
3. THE Application SHALL 使用绿色圆点表示"已连接"状态,红色圆点表示"已断开"状态
|
||||
4. THE Application SHALL 在所有需要等待的操作中显示"loading..."加载提示
|
||||
5. THE Application SHALL 使用 Toast 消息提示用户操作结果和错误信息
|
||||
6. THE Application SHALL 在所有二级页面顶部提供统一的返回按钮和居中标题栏布局
|
||||
7. THE Application SHALL 在所有列表页面支持下拉刷新功能,刷新指示器颜色为 #1A73EC
|
||||
8. WHILE 列表数据支持分页加载, THE Application SHALL 使用 pageSize=20 作为默认分页大小
|
||||
217
.kiro/specs/flutter-to-uniapp-rewrite/tasks.md
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# 实现计划:绥时录 — Flutter 到 UniApp 重写
|
||||
|
||||
## 概述
|
||||
|
||||
将 Flutter 版"绥时录"ODF 端口管理系统完全重写为 UniApp (Vue 3) 应用。按照基础设施 → 核心页面 → 辅助功能的顺序,逐步实现并验证每个模块。
|
||||
|
||||
## 任务列表
|
||||
|
||||
- [x] 1. 项目基础设施搭建
|
||||
- [x] 1.1 配置项目结构和路由
|
||||
- 复制图片资源:将 `app/assets/images/` 下所有图片复制到 `odf-uniapp/static/images/`
|
||||
- 更新 `pages.json`:按设计文档配置 10 个页面路由(start、login、home、region、room、rack、rack-detail、search、settings、change-password),所有页面使用 `navigationStyle: custom`
|
||||
- 更新 `manifest.json`:确认 `vueVersion: "3"` 配置
|
||||
- 配置 `uni.scss`:定义全局样式变量(主题色 `#1A73EC`、背景色等)
|
||||
- 删除默认的 `pages/index/` 目录
|
||||
- 创建所有页面目录和空的 `index.vue` 占位文件
|
||||
- _需求: 14.1, 14.2, 14.6_
|
||||
|
||||
- [x] 1.2 实现全局状态管理 (store/index.js)
|
||||
- 创建 `odf-uniapp/store/index.js`
|
||||
- 使用 Vue 3 `reactive` 实现全局状态对象,包含:token、userId、userName、isPermission、dictUnitTypes、dictBusinessTypes
|
||||
- 实现 `setAuth(token, userId, userName)` 方法,同步写入 `uni.setStorageSync`
|
||||
- 实现 `clearAuth()` 方法,清除状态和本地存储
|
||||
- 初始化时从 `uni.getStorageSync` 恢复认证信息
|
||||
- _需求: 1.1, 2.3, 12.4, 3.9_
|
||||
|
||||
- [ ]* 1.3 编写 store 属性测试
|
||||
- **Property 1: 认证数据持久化往返**
|
||||
- **Property 13: 退出登录清除认证状态**
|
||||
- 创建 `odf-uniapp/tests/store.test.js`,使用 fast-check 生成随机 token/userId/userName 验证存储往返一致性
|
||||
- 验证 clearAuth 后所有认证字段为空
|
||||
- **验证: 需求 1.1, 2.3, 12.4**
|
||||
|
||||
- [x] 1.4 实现 API 通信层 (services/api.js)
|
||||
- 创建 `odf-uniapp/services/api.js`
|
||||
- 封装 `uni.request`,基础地址 `http://49.233.115.141:11082`,超时 20 秒
|
||||
- 每个请求自动携带 `Authorization: Bearer {token}`、`Userid`、`Username` 请求头
|
||||
- 统一解析响应 JSON 的 `code`、`msg`、`data` 字段
|
||||
- 网络异常时返回 `{ code: -1, msg: '网络异常' }`
|
||||
- 导出 `get(url, params)` 和 `post(url, data)` 快捷方法
|
||||
- _需求: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6_
|
||||
|
||||
- [ ]* 1.5 编写 API 通信层属性测试
|
||||
- **Property 9: API 响应解析**
|
||||
- **Property 10: API 异常处理**
|
||||
- **Property 11: API 请求头构造**
|
||||
- 创建 `odf-uniapp/tests/api.test.js`,mock `uni.request` 验证请求头、响应解析和异常处理
|
||||
- **验证: 需求 13.2, 13.4, 13.5**
|
||||
|
||||
- [x] 1.6 实现 API 接口模块
|
||||
- 创建 `odf-uniapp/services/auth.js`:appLogin、checkPermission、updatePassword
|
||||
- 创建 `odf-uniapp/services/home.js`:getCompanyList、getDictUnitType、getDictBusinessType、checkAppVersion
|
||||
- 创建 `odf-uniapp/services/machine.js`:getRegionList、getRoomList、getRackList、getRackDetail、getPortDetail、savePort
|
||||
- 创建 `odf-uniapp/services/search.js`:searchPorts
|
||||
- 所有接口函数按设计文档中的签名实现
|
||||
- _需求: 13.6_
|
||||
|
||||
- [x] 2. 检查点 - 基础设施验证
|
||||
- 确保所有基础模块(store、api、services)代码无语法错误,项目结构完整,请用户确认是否有问题。
|
||||
|
||||
- [x] 3. 启动页与登录流程
|
||||
- [x] 3.1 实现启动页 (pages/start/index.vue)
|
||||
- 页面中央显示应用名称"绥时录"
|
||||
- `onLoad` 时从 store 读取 token
|
||||
- token 存在时调用 `checkPermission()` 验证有效性
|
||||
- 根据返回状态码(200/403/401)设置 isPermission 并导航到对应页面
|
||||
- token 不存在时直接导航至登录页
|
||||
- _需求: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7_
|
||||
|
||||
- [x] 3.2 实现登录页 (pages/login/index.vue)
|
||||
- 背景图片覆盖整个页面,主题色 #1A73EC
|
||||
- 账号输入框(提示"请输入账号")和密码输入框(提示"请输入密码",密码模式)
|
||||
- 点击"登录"按钮调用 `appLogin` 接口
|
||||
- 登录成功:提取 jwt/userId/userName,调用 `store.setAuth()` 存储,检测权限后导航至首页
|
||||
- 登录失败:Toast 显示错误消息
|
||||
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
|
||||
|
||||
- [x] 4. 首页与版本更新
|
||||
- [x] 4.1 实现首页/公司列表 (pages/home/index.vue)
|
||||
- 顶部栏:左侧刷新图标、中间"公司列表"标题、右侧设置图标
|
||||
- 标题栏下方搜索入口栏(提示"请输入要搜索的备注内容"),点击导航至搜索页
|
||||
- `onLoad` 时调用 `getCompanyList()` 获取公司列表,以卡片形式展示 deptName
|
||||
- 同时调用字典接口获取设备型号和业务类型,存入 store
|
||||
- 同时调用版本检查接口
|
||||
- 点击公司卡片携带 deptId 导航至地区列表
|
||||
- 点击设置图标导航至设置页
|
||||
- 支持下拉刷新(`onPullDownRefresh`)
|
||||
- _需求: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10_
|
||||
|
||||
- [x] 4.2 实现版本更新弹窗 (components/update-dialog.vue)
|
||||
- Props: `visible`、`downloadUrl`
|
||||
- 模态弹窗,显示更新图标、"有新版本请更新"提示、"去更新"按钮
|
||||
- 点击"去更新"打开下载链接
|
||||
- 不可通过点击空白或返回键关闭
|
||||
- _需求: 4.1, 4.2, 4.3, 4.4_
|
||||
|
||||
- [x] 5. 层级列表页面(地区 → 机房 → 机架)
|
||||
- [x] 5.1 实现地区列表 (pages/region/index.vue)
|
||||
- 顶部返回按钮 + "地区列表"标题
|
||||
- `onLoad` 接收 deptId 参数,调用 `getRegionList(deptId)` 获取地区列表
|
||||
- 卡片列表展示 deptName,点击携带 deptId 导航至机房列表
|
||||
- _需求: 5.1, 5.2, 5.3, 5.4_
|
||||
|
||||
- [x] 5.2 实现机房列表 (pages/room/index.vue)
|
||||
- 顶部返回按钮 + "机房列表"标题
|
||||
- `onLoad` 接收 deptId 参数,调用 `getRoomList(1, 20, deptId)` 获取机房列表
|
||||
- 卡片展示 roomName、roomAddress、"ODF: N台"
|
||||
- 点击携带 id 和 roomName 导航至机架列表
|
||||
- 支持下拉刷新(重置 pageNum=1)和触底加载更多(距底部 100px)
|
||||
- _需求: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6_
|
||||
|
||||
- [x] 5.3 实现机架列表 (pages/rack/index.vue)
|
||||
- 顶部返回按钮 + "机房详情"标题
|
||||
- `onLoad` 接收 roomId、roomName 参数,调用 `getRackList(1, 20, roomId)` 获取机架列表
|
||||
- 卡片列表展示 rackName,点击携带 id、rackName、roomName 导航至机架详情
|
||||
- 支持下拉刷新和触底加载更多
|
||||
- _需求: 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_
|
||||
|
||||
- [ ]* 5.4 编写分页逻辑属性测试
|
||||
- **Property 3: 分页状态管理**
|
||||
- **Property 12: 默认分页大小**
|
||||
- 创建 `odf-uniapp/tests/pagination.test.js`,验证分页追加逻辑和默认 pageSize=20
|
||||
- **验证: 需求 6.5, 7.5, 11.9, 14.8**
|
||||
|
||||
- [x] 6. 检查点 - 核心导航流程验证
|
||||
- 确保启动页 → 登录 → 首页 → 地区 → 机房 → 机架的完整导航链路代码无误,请用户确认是否有问题。
|
||||
|
||||
- [x] 7. 机架详情与端口编辑
|
||||
- [x] 7.1 实现机架详情页 (pages/rack-detail/index.vue)
|
||||
- 顶部返回按钮 + "{rackName}详情"标题
|
||||
- 标题下方显示 roomName(蓝色 #1A73EC 加粗)
|
||||
- 状态图例:绿色圆点"已连接"、红色圆点"已断开"
|
||||
- `onLoad` 接收 rackId、rackName、roomName 参数,调用 `getRackDetail(rackId)` 获取数据
|
||||
- 加载中显示"loading..."
|
||||
- 按 Frame 分组展示端口矩阵:白色卡片 + Frame 名称 + 按行排列的端口圆形图标
|
||||
- 端口圆形:status=1 绿色、status=0 红色,内显 tips,下显 name
|
||||
- 行内容超出时支持水平滚动
|
||||
- 点击端口弹出 Port_Edit_Dialog
|
||||
- 支持接收搜索页传来的 portId 参数,自动弹出对应端口编辑弹窗
|
||||
- _需求: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8_
|
||||
|
||||
- [ ]* 7.2 编写端口状态颜色映射属性测试
|
||||
- **Property 4: 端口状态颜色映射**
|
||||
- 创建 `odf-uniapp/tests/utils.test.js`,验证 status=1 映射绿色、status=0 映射红色
|
||||
- **验证: 需求 8.6, 14.3**
|
||||
|
||||
- [x] 7.3 实现端口编辑弹窗 (components/port-edit-dialog.vue)
|
||||
- Props: `visible`、`portId`;Events: `@close`、`@saved`
|
||||
- 打开时调用 `getPortDetail(portId)` 加载数据,显示"loading..."
|
||||
- 显示端口位置("位置:{frameName}{name}")和状态圆点
|
||||
- 备注说明多行输入框(5行,提示"请输入备注说明")+ "添加备注"按钮
|
||||
- 光衰信息输入框(提示"请输入光衰信息")
|
||||
- 历史障碍记录区域:已有记录列表(时间选择器 + 故障原因输入框)、"添加新记录"链接、删除按钮
|
||||
- 光缆段信息输入框(提示"请输入光缆段信息")
|
||||
- 权限控制:isPermission=true 显示状态切换按钮(连接/断开)+ 取消/提交按钮;isPermission=false 禁用所有输入 + 仅显示关闭按钮
|
||||
- 提交时校验历史障碍时间,调用 `savePort()` 保存,成功后关闭弹窗并触发父页面刷新
|
||||
- _需求: 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 9.9, 9.10, 9.11, 9.12, 9.13, 9.14, 9.15, 9.16_
|
||||
|
||||
- [ ]* 7.4 编写端口编辑相关属性测试
|
||||
- **Property 5: 权限控制 UI 状态**
|
||||
- **Property 6: 历史障碍记录时间校验**
|
||||
- 创建 `odf-uniapp/tests/permission.test.js` 和 `odf-uniapp/tests/validation.test.js`
|
||||
- **验证: 需求 9.11, 9.12, 9.13, 9.15**
|
||||
|
||||
- [x] 7.5 实现添加备注弹窗 (components/add-note-dialog.vue)
|
||||
- Props: `visible`;Events: `@close`、`@confirm(formattedText)`
|
||||
- 业务名称输入框、设备型号下拉(来源 store.dictUnitTypes)、业务类型下拉(来源 store.dictBusinessTypes)
|
||||
- 三组端口号输入框(仅数字)
|
||||
- 提交校验:业务名称和端口号均不为空
|
||||
- 格式化输出:"{业务名称} {设备型号} {业务类型} {端口号1}/{端口号2}/{端口号3}"
|
||||
- 取消按钮关闭弹窗
|
||||
- _需求: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9, 10.10_
|
||||
|
||||
- [ ]* 7.6 编写添加备注相关属性测试
|
||||
- **Property 7: 备注格式化字符串**
|
||||
- **Property 8: 添加备注表单校验**
|
||||
- 在 `odf-uniapp/tests/validation.test.js` 和 `odf-uniapp/tests/format.test.js` 中添加测试
|
||||
- **验证: 需求 10.6, 10.9**
|
||||
|
||||
- [x] 8. 检查点 - 机架详情与端口编辑验证
|
||||
- 确保机架详情页端口矩阵展示、端口编辑弹窗、添加备注弹窗的完整交互链路代码无误,请用户确认是否有问题。
|
||||
|
||||
- [x] 9. 搜索与设置功能
|
||||
- [x] 9.1 实现搜索页 (pages/search/index.vue)
|
||||
- 顶部返回按钮 + "搜索"标题
|
||||
- 搜索输入框(提示"请输入要搜索的备注内容")+ "搜索"按钮
|
||||
- 点击搜索或键盘搜索键调用 `searchPorts(key, 1, 20)`
|
||||
- 结果分"机房"和"备注信息"两个区域展示
|
||||
- 机房区域:卡片展示 roomName,点击携带 roomId/roomName 导航至机架列表
|
||||
- 备注信息区域:卡片展示 roomName、address、rackName、frameName+name、remarks、opticalAttenuation、historyRemarks、opticalCableOffRemarks、状态圆点
|
||||
- 点击端口卡片携带 rackId/rackName/roomName/portId 导航至机架详情并自动弹出端口编辑弹窗
|
||||
- 触底加载更多端口数据
|
||||
- _需求: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8, 11.9_
|
||||
|
||||
- [x] 9.2 实现设置页 (pages/settings/index.vue)
|
||||
- 顶部返回按钮 + "设置"标题
|
||||
- "修改密码"卡片,点击导航至修改密码页
|
||||
- "退出登录"卡片(红色文字),点击清除 token 并导航至登录页(清除导航栈 `reLaunch`)
|
||||
- _需求: 12.1, 12.2, 12.3, 12.4_
|
||||
|
||||
- [x] 9.3 实现修改密码页 (pages/change-password/index.vue)
|
||||
- 顶部返回按钮 + "修改密码"标题
|
||||
- 旧密码输入框(提示"请输入旧密码")和新密码输入框(提示"请输入新密码")
|
||||
- 点击"确认修改"按钮:校验非空后调用 `updatePassword(oldPassword, newPassword)`
|
||||
- 状态码 200:Toast "修改成功"并返回上一页
|
||||
- 状态码 110:Toast 显示服务器错误消息
|
||||
- _需求: 12.5, 12.6, 12.7, 12.8, 12.9, 12.10, 12.11_
|
||||
|
||||
- [x] 10. 最终检查点 - 全功能验证
|
||||
- 确保所有页面、组件、服务层代码完整且无语法错误,所有导航路径正确连通,请用户确认是否有问题。
|
||||
|
||||
## 备注
|
||||
|
||||
- 标记 `*` 的任务为可选测试任务,可跳过以加快 MVP 进度
|
||||
- 每个任务引用了对应的需求编号,确保需求可追溯
|
||||
- 检查点任务用于阶段性验证,确保增量开发的正确性
|
||||
- 属性测试验证通用正确性属性,单元测试验证具体边界情况
|
||||
17
odf-uniapp/App.vue
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
export default {
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
</style>
|
||||
287
odf-uniapp/components/add-note-dialog.vue
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
<template>
|
||||
<view v-if="visible" class="add-note-overlay">
|
||||
<view class="add-note-content">
|
||||
<!-- 标题 -->
|
||||
<text class="dialog-title">添加备注</text>
|
||||
|
||||
<scroll-view class="scroll-area" scroll-y>
|
||||
<!-- 业务名称 -->
|
||||
<view class="section">
|
||||
<text class="section-label">业务名称</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="businessName"
|
||||
placeholder="请输入业务名称"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 设备型号 -->
|
||||
<view class="section">
|
||||
<text class="section-label">设备型号</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="store.dictUnitTypes"
|
||||
range-key="dictLabel"
|
||||
:value="selectedUnitType"
|
||||
@change="onUnitTypeChange"
|
||||
>
|
||||
<view class="picker-box">
|
||||
<text class="picker-text">{{ unitTypeLabel }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 业务类型 -->
|
||||
<view class="section">
|
||||
<text class="section-label">业务类型</text>
|
||||
<picker
|
||||
mode="selector"
|
||||
:range="store.dictBusinessTypes"
|
||||
range-key="dictLabel"
|
||||
:value="selectedBusinessType"
|
||||
@change="onBusinessTypeChange"
|
||||
>
|
||||
<view class="picker-box">
|
||||
<text class="picker-text">{{ businessTypeLabel }}</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 端口号输入 -->
|
||||
<view class="section">
|
||||
<text class="section-label">端口号</text>
|
||||
<view class="port-inputs">
|
||||
<input
|
||||
class="form-input port-input"
|
||||
v-model="portNumber1"
|
||||
type="number"
|
||||
placeholder="1号端口数"
|
||||
/>
|
||||
<input
|
||||
class="form-input port-input"
|
||||
v-model="portNumber2"
|
||||
type="number"
|
||||
placeholder="2号端口数"
|
||||
/>
|
||||
<input
|
||||
class="form-input port-input"
|
||||
v-model="portNumber3"
|
||||
type="number"
|
||||
placeholder="3号端口数"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="btn-row">
|
||||
<view class="btn btn-cancel" @click="onCancel">
|
||||
<text class="btn-text">取消</text>
|
||||
</view>
|
||||
<view class="btn btn-submit" @click="onSubmit">
|
||||
<text class="btn-text-white">提交</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import store from '@/store'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'confirm'])
|
||||
|
||||
const businessName = ref('')
|
||||
const selectedUnitType = ref(0)
|
||||
const selectedBusinessType = ref(0)
|
||||
const portNumber1 = ref('')
|
||||
const portNumber2 = ref('')
|
||||
const portNumber3 = ref('')
|
||||
|
||||
const unitTypeLabel = computed(() => {
|
||||
if (store.dictUnitTypes.length > 0 && selectedUnitType.value < store.dictUnitTypes.length) {
|
||||
return store.dictUnitTypes[selectedUnitType.value].dictLabel
|
||||
}
|
||||
return '请选择'
|
||||
})
|
||||
|
||||
const businessTypeLabel = computed(() => {
|
||||
if (store.dictBusinessTypes.length > 0 && selectedBusinessType.value < store.dictBusinessTypes.length) {
|
||||
return store.dictBusinessTypes[selectedBusinessType.value].dictLabel
|
||||
}
|
||||
return '请选择'
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// Reset form and default select first item
|
||||
businessName.value = ''
|
||||
selectedUnitType.value = 0
|
||||
selectedBusinessType.value = 0
|
||||
portNumber1.value = ''
|
||||
portNumber2.value = ''
|
||||
portNumber3.value = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function onUnitTypeChange(e) {
|
||||
selectedUnitType.value = Number(e.detail.value)
|
||||
}
|
||||
|
||||
function onBusinessTypeChange(e) {
|
||||
selectedBusinessType.value = Number(e.detail.value)
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
if (!businessName.value.trim()) {
|
||||
uni.showToast({ title: '请输入业务名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!portNumber1.value || !portNumber2.value || !portNumber3.value) {
|
||||
uni.showToast({ title: '请输入端口号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const unitLabel = store.dictUnitTypes.length > 0
|
||||
? store.dictUnitTypes[selectedUnitType.value].dictLabel
|
||||
: ''
|
||||
const bizLabel = store.dictBusinessTypes.length > 0
|
||||
? store.dictBusinessTypes[selectedBusinessType.value].dictLabel
|
||||
: ''
|
||||
|
||||
const formatted = `${businessName.value} ${unitLabel} ${bizLabel} ${portNumber1.value}/${portNumber2.value}/${portNumber3.value}`
|
||||
emit('confirm', formatted)
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add-note-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.add-note-content {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.scroll-area {
|
||||
flex: 1;
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 26rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.picker-box {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.port-inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.port-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #f5f5f5;
|
||||
border: 1rpx solid #ddd;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background-color: #1A73EC;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-text-white {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
564
odf-uniapp/components/port-edit-dialog.vue
Normal file
|
|
@ -0,0 +1,564 @@
|
|||
<template>
|
||||
<view v-if="visible" class="port-edit-overlay" @click.self="onOverlayClick">
|
||||
<view class="port-edit-content">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text class="loading-text">loading...</text>
|
||||
</view>
|
||||
|
||||
<scroll-view v-else class="scroll-area" scroll-y>
|
||||
<!-- 端口位置和状态 -->
|
||||
<view class="section">
|
||||
<view class="location-row">
|
||||
<text class="location-text">位置:{{ portData.frameName }}{{ portData.name }}</text>
|
||||
<view class="status-badge">
|
||||
<view class="status-dot" :class="portData.status === 1 ? 'dot-green' : 'dot-red'" />
|
||||
<text class="status-label">{{ portData.status === 1 ? '已连接' : '已断开' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注说明 -->
|
||||
<view class="section">
|
||||
<text class="section-title">备注说明</text>
|
||||
<view class="remarks-row">
|
||||
<textarea
|
||||
class="remarks-input"
|
||||
v-model="form.remarks"
|
||||
:maxlength="-1"
|
||||
placeholder="请输入备注说明"
|
||||
:disabled="!store.isPermission"
|
||||
:rows="5"
|
||||
/>
|
||||
<view v-if="store.isPermission" class="add-note-btn" @click="showAddNote = true">
|
||||
<text class="add-note-text">添加备注</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 光衰信息 -->
|
||||
<view class="section">
|
||||
<text class="section-title">光衰信息</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.opticalAttenuation"
|
||||
placeholder="请输入光衰信息"
|
||||
:disabled="!store.isPermission"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 历史障碍记录 -->
|
||||
<view class="section">
|
||||
<text class="section-title">历史障碍记录</text>
|
||||
<view class="fault-list">
|
||||
<view class="fault-item" v-for="(item, index) in form.historyFault" :key="index">
|
||||
<view class="fault-row">
|
||||
<picker
|
||||
mode="date"
|
||||
:value="item.faultTime ? item.faultTime.substring(0, 10) : ''"
|
||||
:disabled="!store.isPermission"
|
||||
@change="onFaultDateChange($event, index)"
|
||||
>
|
||||
<view class="date-picker">
|
||||
<text :class="item.faultTime ? 'date-text' : 'date-placeholder'">
|
||||
{{ item.faultTime || '选择日期' }}
|
||||
</text>
|
||||
</view>
|
||||
</picker>
|
||||
<input
|
||||
class="fault-reason-input"
|
||||
v-model="item.faultReason"
|
||||
placeholder="故障原因"
|
||||
:disabled="!store.isPermission"
|
||||
/>
|
||||
<view v-if="store.isPermission" class="delete-btn" @click="removeFault(index)">
|
||||
<text class="delete-btn-text">-</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="store.isPermission" class="add-record-link" @click="addFault">
|
||||
<text class="add-record-text">添加新记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 光缆段信息 -->
|
||||
<view class="section">
|
||||
<text class="section-title">光缆段信息</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="form.opticalCableOffRemarks"
|
||||
placeholder="请输入光缆段信息"
|
||||
:disabled="!store.isPermission"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 权限控制区域 -->
|
||||
<view v-if="store.isPermission" class="section">
|
||||
<text class="section-title">改变状态</text>
|
||||
<view class="status-toggle-row">
|
||||
<view
|
||||
class="toggle-btn toggle-green"
|
||||
:class="{ 'toggle-active': form.status === 1 }"
|
||||
@click="setStatus(1)"
|
||||
>
|
||||
<text class="toggle-text">连接</text>
|
||||
</view>
|
||||
<view
|
||||
class="toggle-btn toggle-red"
|
||||
:class="{ 'toggle-active': form.status === 0 }"
|
||||
@click="setStatus(0)"
|
||||
>
|
||||
<text class="toggle-text">断开</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hint-text">断开后只清空备注说明,其他内容不影响</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="btn-row">
|
||||
<template v-if="store.isPermission">
|
||||
<view class="btn btn-cancel" @click="onClose">
|
||||
<text class="btn-text">取消</text>
|
||||
</view>
|
||||
<view class="btn btn-submit" @click="onSubmit">
|
||||
<text class="btn-text-white">提交</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="btn btn-cancel btn-full" @click="onClose">
|
||||
<text class="btn-text">关闭</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 添加备注弹窗 -->
|
||||
<addNoteDialog
|
||||
:visible="showAddNote"
|
||||
@close="showAddNote = false"
|
||||
@confirm="onNoteConfirm"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { getPortDetail, savePort } from '@/services/machine'
|
||||
import store from '@/store'
|
||||
import addNoteDialog from '@/components/add-note-dialog.vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
portId: { type: [Number, String], default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'saved'])
|
||||
|
||||
const loading = ref(false)
|
||||
const showAddNote = ref(false)
|
||||
|
||||
const portData = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
frameName: '',
|
||||
status: 0,
|
||||
remarks: '',
|
||||
opticalAttenuation: '',
|
||||
opticalCableOffRemarks: '',
|
||||
historyRemarks: '',
|
||||
historyFault: []
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
status: 0,
|
||||
remarks: '',
|
||||
opticalAttenuation: '',
|
||||
opticalCableOffRemarks: '',
|
||||
historyRemarks: '',
|
||||
historyFault: []
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.portId) {
|
||||
loadPortDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function loadPortDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getPortDetail(props.portId)
|
||||
if (res.code === 200 && res.data) {
|
||||
const d = res.data
|
||||
Object.assign(portData, {
|
||||
id: d.id,
|
||||
name: d.name || '',
|
||||
frameName: d.frameName || '',
|
||||
status: d.status,
|
||||
remarks: d.remarks || '',
|
||||
opticalAttenuation: d.opticalAttenuation || '',
|
||||
opticalCableOffRemarks: d.opticalCableOffRemarks || '',
|
||||
historyRemarks: d.historyRemarks || '',
|
||||
historyFault: d.historyFault || []
|
||||
})
|
||||
// 初始化表单
|
||||
form.status = d.status
|
||||
form.remarks = d.remarks || ''
|
||||
form.opticalAttenuation = d.opticalAttenuation || ''
|
||||
form.opticalCableOffRemarks = d.opticalCableOffRemarks || ''
|
||||
form.historyRemarks = d.historyRemarks || ''
|
||||
form.historyFault = (d.historyFault || []).map(item => ({
|
||||
faultTime: item.faultTime || '',
|
||||
faultReason: item.faultReason || ''
|
||||
}))
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
form.status = status
|
||||
if (status === 0) {
|
||||
form.remarks = ''
|
||||
}
|
||||
}
|
||||
|
||||
function addFault() {
|
||||
form.historyFault.push({ faultTime: '', faultReason: '' })
|
||||
}
|
||||
|
||||
function removeFault(index) {
|
||||
form.historyFault.splice(index, 1)
|
||||
}
|
||||
|
||||
function onFaultDateChange(e, index) {
|
||||
form.historyFault[index].faultTime = e.detail.value
|
||||
}
|
||||
|
||||
function onNoteConfirm(text) {
|
||||
form.remarks = form.remarks ? form.remarks + '\n' + text : text
|
||||
showAddNote.value = false
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function onOverlayClick() {
|
||||
// 不做任何操作,防止误触关闭
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
// 校验历史障碍时间
|
||||
for (let i = 0; i < form.historyFault.length; i++) {
|
||||
if (!form.historyFault[i].faultTime || !form.historyFault[i].faultTime.trim()) {
|
||||
uni.showToast({ title: '请选择障碍发生时间!', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await savePort({
|
||||
Id: portData.id,
|
||||
Status: form.status,
|
||||
Remarks: form.remarks,
|
||||
OpticalAttenuation: form.opticalAttenuation,
|
||||
HistoryRemarks: form.historyRemarks,
|
||||
HistoryFault: form.historyFault,
|
||||
OpticalCableOffRemarks: form.opticalCableOffRemarks
|
||||
})
|
||||
if (res.code === 200) {
|
||||
uni.showToast({ title: '保存成功', icon: 'success' })
|
||||
emit('close')
|
||||
emit('saved')
|
||||
} else {
|
||||
uni.showToast({ title: res.msg || '保存失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '网络异常', icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.port-edit-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.port-edit-content {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.scroll-area {
|
||||
max-height: 85vh;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.location-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.dot-green {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.dot-red {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.remarks-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.remarks-input {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 16rpx;
|
||||
font-size: 26rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.add-note-btn {
|
||||
align-self: flex-end;
|
||||
padding: 10rpx 24rpx;
|
||||
background-color: #1A73EC;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.add-note-text {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 26rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.fault-list {
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.fault-item {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.fault-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
flex-shrink: 0;
|
||||
width: 240rpx;
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.date-placeholder {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.fault-reason-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border: 1rpx solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 16rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #F44336;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.delete-btn-text {
|
||||
font-size: 36rpx;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.add-record-link {
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.add-record-text {
|
||||
font-size: 26rpx;
|
||||
color: #1A73EC;
|
||||
}
|
||||
|
||||
.status-toggle-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.toggle-green {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.toggle-red {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
.toggle-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #f5f5f5;
|
||||
border: 1rpx solid #ddd;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background-color: #1A73EC;
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-text-white {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
82
odf-uniapp/components/update-dialog.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<view class="update-mask" v-if="visible" @click.stop>
|
||||
<view class="update-dialog" @click.stop>
|
||||
<image class="update-icon" src="/static/images/ic_update.png" mode="aspectFit" />
|
||||
<text class="update-title">有新版本请更新</text>
|
||||
<view class="update-btn" @click="handleUpdate">
|
||||
<text class="update-btn-text">去更新</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
downloadUrl: { type: String, default: '' }
|
||||
})
|
||||
|
||||
function handleUpdate() {
|
||||
if (!props.downloadUrl) return
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL(props.downloadUrl)
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
window.open(props.downloadUrl)
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.update-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.update-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 560rpx;
|
||||
padding: 60rpx 40rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.update-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.update-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.update-btn {
|
||||
width: 400rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #1A73EC;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.update-btn-text {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
20
odf-uniapp/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
22
odf-uniapp/main.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import App from './App'
|
||||
|
||||
// #ifndef VUE3
|
||||
import Vue from 'vue'
|
||||
import './uni.promisify.adaptor'
|
||||
Vue.config.productionTip = false
|
||||
App.mpType = 'app'
|
||||
const app = new Vue({
|
||||
...App
|
||||
})
|
||||
app.$mount()
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE3
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
72
odf-uniapp/manifest.json
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
"name" : "odf-uniapp",
|
||||
"appid" : "__UNI__45FFD83",
|
||||
"description" : "",
|
||||
"versionName" : "1.0.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
"app-plus" : {
|
||||
"usingComponents" : true,
|
||||
"nvueStyleCompiler" : "uni-app",
|
||||
"compilerVersion" : 3,
|
||||
"splashscreen" : {
|
||||
"alwaysShowBeforeRender" : true,
|
||||
"waiting" : true,
|
||||
"autoclose" : true,
|
||||
"delay" : 0
|
||||
},
|
||||
/* 模块配置 */
|
||||
"modules" : {},
|
||||
/* 应用发布信息 */
|
||||
"distribute" : {
|
||||
/* android打包配置 */
|
||||
"android" : {
|
||||
"permissions" : [
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||
]
|
||||
},
|
||||
/* ios打包配置 */
|
||||
"ios" : {},
|
||||
/* SDK配置 */
|
||||
"sdkConfigs" : {}
|
||||
}
|
||||
},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-alipay" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-baidu" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"mp-toutiao" : {
|
||||
"usingComponents" : true
|
||||
},
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
50
odf-uniapp/pages.json
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/start/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/home/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "", "enablePullDownRefresh": true }
|
||||
},
|
||||
{
|
||||
"path": "pages/region/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/room/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "", "enablePullDownRefresh": true }
|
||||
},
|
||||
{
|
||||
"path": "pages/rack/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "", "enablePullDownRefresh": true }
|
||||
},
|
||||
{
|
||||
"path": "pages/rack-detail/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/search/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/settings/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
},
|
||||
{
|
||||
"path": "pages/change-password/index",
|
||||
"style": { "navigationStyle": "custom", "navigationBarTitleText": "" }
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "绥时录",
|
||||
"navigationBarBackgroundColor": "#1A73EC",
|
||||
"backgroundColor": "#F5F5F5"
|
||||
}
|
||||
}
|
||||
179
odf-uniapp/pages/change-password/index.vue
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<template>
|
||||
<view class="change-password-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">修改密码</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<view class="form-area">
|
||||
<view class="input-wrap">
|
||||
<input
|
||||
class="input-field"
|
||||
v-model="oldPassword"
|
||||
placeholder="请输入旧密码"
|
||||
placeholder-class="placeholder"
|
||||
password
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-wrap">
|
||||
<input
|
||||
class="input-field"
|
||||
v-model="newPassword"
|
||||
placeholder="请输入新密码"
|
||||
placeholder-class="placeholder"
|
||||
password
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="submit-btn" @click="handleSubmit">
|
||||
<text class="submit-btn-text">确认修改</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { updatePassword } from '@/services/auth'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
|
||||
const oldPassword = ref('')
|
||||
const newPassword = ref('')
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!oldPassword.value) {
|
||||
uni.showToast({ title: '请输入旧密码!', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!newPassword.value) {
|
||||
uni.showToast({ title: '请输入新密码!', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const res = await updatePassword(oldPassword.value, newPassword.value)
|
||||
if (res.code === 200) {
|
||||
uni.showToast({ title: '修改成功', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else if (res.code === 110) {
|
||||
uni.showToast({ title: res.msg, icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.change-password-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form-area {
|
||||
padding: 40rpx 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
width: 560rpx;
|
||||
height: 90rpx;
|
||||
background-color: #ECEFF3;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 60rpx;
|
||||
width: 560rpx;
|
||||
height: 90rpx;
|
||||
background-color: #1A73EC;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submit-btn-text {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
203
odf-uniapp/pages/home/index.vue
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<template>
|
||||
<view class="home-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_refresh.png"
|
||||
mode="aspectFit"
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
<text class="nav-title">公司列表</text>
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_set.png"
|
||||
mode="aspectFit"
|
||||
@click="goSettings"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索入口栏 -->
|
||||
<view class="search-bar" @click="goSearch">
|
||||
<image class="search-icon" src="/static/images/ic_search.png" mode="aspectFit" />
|
||||
<text class="search-placeholder">请输入要搜索的备注内容</text>
|
||||
</view>
|
||||
|
||||
<!-- 公司列表 -->
|
||||
<scroll-view class="company-list" scroll-y>
|
||||
<view
|
||||
class="company-card"
|
||||
v-for="item in companyList"
|
||||
:key="item.deptId"
|
||||
@click="goRegion(item)"
|
||||
>
|
||||
<text class="company-name">{{ item.deptName }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 版本更新弹窗 -->
|
||||
<update-dialog :visible="showUpdate" :downloadUrl="updateUrl" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import store from '@/store'
|
||||
import { getCompanyList, getDictUnitType, getDictBusinessType, checkAppVersion } from '@/services/home'
|
||||
import updateDialog from '@/components/update-dialog.vue'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const companyList = ref([])
|
||||
const showUpdate = ref(false)
|
||||
const updateUrl = ref('')
|
||||
|
||||
async function loadCompanyList() {
|
||||
const res = await getCompanyList()
|
||||
if (res.code === 200) {
|
||||
companyList.value = res.data || []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDictData() {
|
||||
const [unitRes, bizRes] = await Promise.all([
|
||||
getDictUnitType(),
|
||||
getDictBusinessType()
|
||||
])
|
||||
if (unitRes.code === 200) {
|
||||
store.dictUnitTypes = unitRes.data || []
|
||||
}
|
||||
if (bizRes.code === 200) {
|
||||
store.dictBusinessTypes = bizRes.data || []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVersionCheck() {
|
||||
const res = await checkAppVersion()
|
||||
if (res.code === 200 && res.data && res.data.needUpdate) {
|
||||
updateUrl.value = res.data.downloadUrl || ''
|
||||
showUpdate.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
loadCompanyList()
|
||||
}
|
||||
|
||||
function goSearch() {
|
||||
uni.navigateTo({ url: '/pages/search/index' })
|
||||
}
|
||||
|
||||
function goSettings() {
|
||||
uni.navigateTo({ url: '/pages/settings/index' })
|
||||
}
|
||||
|
||||
function goRegion(item) {
|
||||
uni.navigateTo({ url: '/pages/region/index?deptId=' + item.deptId })
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadCompanyList()
|
||||
loadDictData()
|
||||
loadVersionCheck()
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
loadCompanyList().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 16rpx 24rpx;
|
||||
padding: 0 24rpx;
|
||||
height: 72rpx;
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.company-list {
|
||||
padding: 16rpx 24rpx;
|
||||
height: calc(100vh - 400rpx);
|
||||
}
|
||||
|
||||
.company-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.company-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
109
odf-uniapp/pages/login/index.vue
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<view class="login-page">
|
||||
<text class="app-title">绥时录</text>
|
||||
|
||||
<view class="input-wrap">
|
||||
<input
|
||||
class="input-field"
|
||||
v-model="username"
|
||||
placeholder="请输入账号"
|
||||
placeholder-class="placeholder"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="input-wrap">
|
||||
<input
|
||||
class="input-field"
|
||||
v-model="password"
|
||||
placeholder="请输入密码"
|
||||
placeholder-class="placeholder"
|
||||
password
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="login-btn" @click="handleLogin">
|
||||
<text class="login-btn-text">登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import store from '@/store'
|
||||
import { appLogin, checkPermission } from '@/services/auth'
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
|
||||
async function handleLogin() {
|
||||
const res = await appLogin(username.value, password.value)
|
||||
if (res.code === 200) {
|
||||
const { jwt, userId, userName } = res.data
|
||||
store.setAuth(jwt, userId, userName)
|
||||
const permRes = await checkPermission()
|
||||
store.isPermission = permRes.code === 200
|
||||
uni.reLaunch({ url: '/pages/home/index' })
|
||||
} else {
|
||||
uni.showToast({ title: res.msg, icon: 'none' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-image: url('/static/images/login_bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
margin-top: 200rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
margin-top: 60rpx;
|
||||
width: 560rpx;
|
||||
height: 90rpx;
|
||||
background-color: #ECEFF3;
|
||||
border: 1rpx solid rgba(0, 0, 0, 0.08);
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
margin-top: 160rpx;
|
||||
width: 560rpx;
|
||||
height: 90rpx;
|
||||
background-color: #1A73EC;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-btn-text {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
327
odf-uniapp/pages/rack-detail/index.vue
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
<template>
|
||||
<view class="rack-detail-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">{{ rackName }}详情</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 机房名称 -->
|
||||
<view class="room-name-bar">
|
||||
<text class="room-name-text">{{ roomName }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 状态图例 -->
|
||||
<view class="legend-bar">
|
||||
<view class="legend-item">
|
||||
<view class="legend-dot legend-dot-green" />
|
||||
<text class="legend-label">已连接</text>
|
||||
</view>
|
||||
<view class="legend-item">
|
||||
<view class="legend-dot legend-dot-red" />
|
||||
<text class="legend-label">已断开</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="loading-box">
|
||||
<text class="loading-text">loading...</text>
|
||||
</view>
|
||||
|
||||
<!-- Frame 列表 -->
|
||||
<view v-else class="frame-list">
|
||||
<view class="frame-card" v-for="frame in frameList" :key="frame.id">
|
||||
<text class="frame-name">{{ frame.name }}</text>
|
||||
|
||||
<!-- 端口行 -->
|
||||
<view class="port-row" v-for="(row, rowIdx) in frame.odfPortsList" :key="rowIdx">
|
||||
<text class="row-name">{{ row.name }}</text>
|
||||
<scroll-view class="port-scroll" scroll-x>
|
||||
<view class="port-list">
|
||||
<view
|
||||
class="port-item"
|
||||
v-for="port in row.rowList"
|
||||
:key="port.id"
|
||||
@click="openPortEdit(port)"
|
||||
>
|
||||
<view
|
||||
class="port-circle"
|
||||
:class="port.status === 1 ? 'port-green' : 'port-red'"
|
||||
>
|
||||
<text class="port-tips">{{ port.tips }}</text>
|
||||
</view>
|
||||
<text class="port-name">{{ port.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 端口编辑弹窗 -->
|
||||
<portEditDialog
|
||||
:visible="showPortEdit"
|
||||
:portId="currentPortId"
|
||||
@close="showPortEdit = false"
|
||||
@saved="onPortSaved"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getRackDetail } from '@/services/machine'
|
||||
import store from '@/store'
|
||||
import portEditDialog from '@/components/port-edit-dialog.vue'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const rackId = ref('')
|
||||
const rackName = ref('')
|
||||
const roomName = ref('')
|
||||
const frameList = ref([])
|
||||
const loading = ref(false)
|
||||
const showPortEdit = ref(false)
|
||||
const currentPortId = ref('')
|
||||
let pendingPortId = ''
|
||||
|
||||
async function loadRackDetail() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRackDetail(rackId.value)
|
||||
if (res.code === 200 && res.data) {
|
||||
frameList.value = res.data
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 如果有待打开的端口(从搜索页跳转),加载完成后自动弹出
|
||||
if (pendingPortId) {
|
||||
currentPortId.value = pendingPortId
|
||||
showPortEdit.value = true
|
||||
pendingPortId = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function openPortEdit(port) {
|
||||
currentPortId.value = port.id
|
||||
showPortEdit.value = true
|
||||
}
|
||||
|
||||
function onPortSaved() {
|
||||
showPortEdit.value = false
|
||||
loadRackDetail()
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.rackId) {
|
||||
rackId.value = options.rackId
|
||||
}
|
||||
if (options.rackName) {
|
||||
rackName.value = decodeURIComponent(options.rackName)
|
||||
}
|
||||
if (options.roomName) {
|
||||
roomName.value = decodeURIComponent(options.roomName)
|
||||
}
|
||||
if (options.portId) {
|
||||
pendingPortId = options.portId
|
||||
}
|
||||
loadRackDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rack-detail-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.room-name-bar {
|
||||
padding: 8rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.room-name-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1A73EC;
|
||||
}
|
||||
|
||||
.legend-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.legend-dot-green {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.legend-dot-red {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 60rpx 0;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.frame-list {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.frame-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.frame-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.port-row {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.row-name {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.port-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.port-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.port-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.port-circle {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.port-green {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.port-red {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
.port-tips {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.port-name {
|
||||
font-size: 20rpx;
|
||||
color: #333;
|
||||
margin-top: 6rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
169
odf-uniapp/pages/rack/index.vue
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<template>
|
||||
<view class="rack-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">机房详情</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 机架列表 -->
|
||||
<view class="rack-list">
|
||||
<view
|
||||
class="rack-card"
|
||||
v-for="item in rackList"
|
||||
:key="item.id"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<text class="rack-name">{{ item.rackName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getRackList } from '@/services/machine'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const rackList = ref([])
|
||||
const roomIdRef = ref('')
|
||||
const roomName = ref('')
|
||||
const pageNum = ref(1)
|
||||
const pageSize = 20
|
||||
const totalPage = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadRackList(isLoadMore = false) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRackList(pageNum.value, pageSize, roomIdRef.value)
|
||||
if (res.code === 200 && res.data) {
|
||||
totalPage.value = res.data.totalPage || 0
|
||||
if (isLoadMore) {
|
||||
rackList.value = [...rackList.value, ...(res.data.result || [])]
|
||||
} else {
|
||||
rackList.value = res.data.result || []
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/rack-detail/index?rackId=' + item.id + '&rackName=' + encodeURIComponent(item.rackName) + '&roomName=' + encodeURIComponent(roomName.value)
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.roomId) {
|
||||
roomIdRef.value = options.roomId
|
||||
}
|
||||
if (options.roomName) {
|
||||
roomName.value = decodeURIComponent(options.roomName)
|
||||
}
|
||||
loadRackList()
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
pageNum.value = 1
|
||||
loadRackList().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (pageNum.value >= totalPage.value) return
|
||||
pageNum.value++
|
||||
loadRackList(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rack-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rack-list {
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.rack-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.rack-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
134
odf-uniapp/pages/region/index.vue
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<view class="region-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">地区列表</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 地区列表 -->
|
||||
<scroll-view class="region-list" scroll-y>
|
||||
<view
|
||||
class="region-card"
|
||||
v-for="item in regionList"
|
||||
:key="item.deptId"
|
||||
@click="goRoom(item)"
|
||||
>
|
||||
<text class="region-name">{{ item.deptName }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getRegionList } from '@/services/machine'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const regionList = ref([])
|
||||
|
||||
async function loadRegionList(deptId) {
|
||||
const res = await getRegionList(deptId)
|
||||
if (res.code === 200) {
|
||||
regionList.value = res.data || []
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goRoom(item) {
|
||||
uni.navigateTo({ url: '/pages/room/index?deptId=' + item.deptId })
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.deptId) {
|
||||
loadRegionList(options.deptId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.region-list {
|
||||
padding: 16rpx 24rpx;
|
||||
height: calc(100vh - 300rpx);
|
||||
}
|
||||
|
||||
.region-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.region-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
179
odf-uniapp/pages/room/index.vue
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<template>
|
||||
<view class="room-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">机房列表</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 机房列表 -->
|
||||
<view class="room-list">
|
||||
<view
|
||||
class="room-card"
|
||||
v-for="item in roomList"
|
||||
:key="item.id"
|
||||
@click="goRack(item)"
|
||||
>
|
||||
<text class="room-name">{{ item.roomName }}</text>
|
||||
<text class="room-address">{{ item.roomAddress }}</text>
|
||||
<text class="room-odf">ODF: {{ item.racksCount }}台</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getRoomList } from '@/services/machine'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const roomList = ref([])
|
||||
const deptIdRef = ref('')
|
||||
const pageNum = ref(1)
|
||||
const pageSize = 20
|
||||
const totalPage = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
async function loadRoomList(isLoadMore = false) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getRoomList(pageNum.value, pageSize, deptIdRef.value)
|
||||
if (res.code === 200 && res.data) {
|
||||
totalPage.value = res.data.totalPage || 0
|
||||
if (isLoadMore) {
|
||||
roomList.value = [...roomList.value, ...(res.data.result || [])]
|
||||
} else {
|
||||
roomList.value = res.data.result || []
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goRack(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/rack/index?roomId=' + item.id + '&roomName=' + encodeURIComponent(item.roomName)
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.deptId) {
|
||||
deptIdRef.value = options.deptId
|
||||
loadRoomList()
|
||||
}
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
pageNum.value = 1
|
||||
loadRoomList().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (pageNum.value >= totalPage.value) return
|
||||
pageNum.value++
|
||||
loadRoomList(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.room-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.room-list {
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.room-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.room-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.room-address {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.room-odf {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
360
odf-uniapp/pages/search/index.vue
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
<template>
|
||||
<view class="search-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">搜索</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<input
|
||||
class="search-input"
|
||||
v-model="keyword"
|
||||
placeholder="请输入要搜索的备注内容"
|
||||
confirm-type="search"
|
||||
@confirm="doSearch"
|
||||
/>
|
||||
<view class="search-btn" @click="doSearch">
|
||||
<text class="search-btn-text">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果 -->
|
||||
<view class="result-area" v-if="searched">
|
||||
<!-- 机房区域 -->
|
||||
<view class="section" v-if="rooms.length > 0">
|
||||
<text class="section-title">机房</text>
|
||||
<view
|
||||
class="room-card"
|
||||
v-for="item in rooms"
|
||||
:key="item.roomId"
|
||||
@click="goRack(item)"
|
||||
>
|
||||
<text class="room-card-name">{{ item.roomName }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注信息区域 -->
|
||||
<view class="section" v-if="ports.length > 0">
|
||||
<text class="section-title">备注信息</text>
|
||||
<view
|
||||
class="port-card"
|
||||
v-for="item in ports"
|
||||
:key="item.id"
|
||||
@click="goPortDetail(item)"
|
||||
>
|
||||
<view class="port-card-row">
|
||||
<text class="port-label">机房:</text>
|
||||
<text class="port-value">{{ item.roomName }}</text>
|
||||
</view>
|
||||
<view class="port-card-row" v-if="item.address">
|
||||
<text class="port-label">地址:</text>
|
||||
<text class="port-value">{{ item.address }}</text>
|
||||
</view>
|
||||
<view class="port-card-row">
|
||||
<text class="port-label">ODF名称:</text>
|
||||
<text class="port-value">{{ item.rackName }}</text>
|
||||
</view>
|
||||
<view class="port-card-row">
|
||||
<text class="port-label">点位置:</text>
|
||||
<text class="port-value">{{ item.frameName }}{{ item.name }}</text>
|
||||
</view>
|
||||
<view class="port-card-row" v-if="item.remarks">
|
||||
<text class="port-label">备注:</text>
|
||||
<text class="port-value">{{ item.remarks }}</text>
|
||||
</view>
|
||||
<view class="port-card-row" v-if="item.opticalAttenuation">
|
||||
<text class="port-label">光衰信息:</text>
|
||||
<text class="port-value">{{ item.opticalAttenuation }}</text>
|
||||
</view>
|
||||
<view class="port-card-row" v-if="item.historyRemarks">
|
||||
<text class="port-label">历史故障:</text>
|
||||
<text class="port-value">{{ item.historyRemarks }}</text>
|
||||
</view>
|
||||
<view class="port-card-row" v-if="item.opticalCableOffRemarks">
|
||||
<text class="port-label">光缆段信息:</text>
|
||||
<text class="port-value">{{ item.opticalCableOffRemarks }}</text>
|
||||
</view>
|
||||
<view class="port-card-row">
|
||||
<text class="port-label">状态:</text>
|
||||
<view class="status-wrap">
|
||||
<view
|
||||
class="status-dot"
|
||||
:class="item.status === 1 ? 'status-green' : 'status-red'"
|
||||
/>
|
||||
<text class="status-text">{{ item.status === 1 ? '已连接' : '已断开' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无结果 -->
|
||||
<view class="no-result" v-if="rooms.length === 0 && ports.length === 0">
|
||||
<text class="no-result-text">暂无搜索结果</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { searchPorts } from '@/services/search'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
const keyword = ref('')
|
||||
const rooms = ref([])
|
||||
const ports = ref([])
|
||||
const searched = ref(false)
|
||||
const pageNum = ref(1)
|
||||
const pageSize = 20
|
||||
const totalPage = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
async function doSearch() {
|
||||
const key = keyword.value.trim()
|
||||
if (!key) return
|
||||
pageNum.value = 1
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await searchPorts(key, 1, pageSize)
|
||||
if (res.code === 200 && res.data) {
|
||||
rooms.value = res.data.rooms || []
|
||||
const portsData = res.data.ports || {}
|
||||
ports.value = portsData.result || []
|
||||
totalPage.value = portsData.totalPage || 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
searched.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (loading.value) return
|
||||
if (pageNum.value >= totalPage.value) return
|
||||
loading.value = true
|
||||
pageNum.value++
|
||||
try {
|
||||
const res = await searchPorts(keyword.value.trim(), pageNum.value, pageSize)
|
||||
if (res.code === 200 && res.data) {
|
||||
const portsData = res.data.ports || {}
|
||||
ports.value = [...ports.value, ...(portsData.result || [])]
|
||||
totalPage.value = portsData.totalPage || 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goRack(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/rack/index?roomId=' + item.roomId + '&roomName=' + encodeURIComponent(item.roomName)
|
||||
})
|
||||
}
|
||||
|
||||
function goPortDetail(item) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/rack-detail/index?rackId=' + item.rackId + '&rackName=' + encodeURIComponent(item.rackName) + '&roomName=' + encodeURIComponent(item.roomName) + '&portId=' + item.id
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
padding: 0 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
margin-left: 16rpx;
|
||||
padding: 0 28rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #1A73EC;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.search-btn-text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.result-area {
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.room-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.room-card-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.port-card {
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.port-card-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.port-card-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.port-label {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.port-value {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.status-green {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.status-red {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.no-result {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.no-result-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
125
odf-uniapp/pages/settings/index.vue
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<view class="settings-page">
|
||||
<image class="bg-image" src="/static/images/home_bg.png" mode="aspectFill" />
|
||||
|
||||
<view class="content">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="nav-bar-inner">
|
||||
<image
|
||||
class="nav-icon"
|
||||
src="/static/images/ic_back.png"
|
||||
mode="aspectFit"
|
||||
@click="goBack"
|
||||
/>
|
||||
<text class="nav-title">设置</text>
|
||||
<view class="nav-icon-placeholder" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设置选项 -->
|
||||
<view class="settings-list">
|
||||
<view class="settings-card" @click="goChangePassword">
|
||||
<text class="settings-label">修改密码</text>
|
||||
</view>
|
||||
|
||||
<view class="settings-card" @click="handleLogout">
|
||||
<text class="settings-label logout-text">退出登录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import store from '@/store'
|
||||
|
||||
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
function goChangePassword() {
|
||||
uni.navigateTo({ url: '/pages/change-password/index' })
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
store.clearAuth()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 500rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-bar-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-icon-placeholder {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
padding: 16rpx 24rpx;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
color: #E53935;
|
||||
}
|
||||
</style>
|
||||
47
odf-uniapp/pages/start/index.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<view class="start-page">
|
||||
<text class="app-name">绥时录</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import store from '@/store'
|
||||
import { checkPermission } from '@/services/auth'
|
||||
|
||||
onLoad(() => {
|
||||
if (store.token) {
|
||||
checkPermission().then((res) => {
|
||||
if (res.code === 200) {
|
||||
store.isPermission = true
|
||||
uni.reLaunch({ url: '/pages/home/index' })
|
||||
} else if (res.code === 403) {
|
||||
store.isPermission = false
|
||||
uni.reLaunch({ url: '/pages/home/index' })
|
||||
} else if (res.code === 401) {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
}).catch(() => {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
})
|
||||
} else {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.start-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #1A73EC;
|
||||
}
|
||||
</style>
|
||||
42
odf-uniapp/services/api.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// services/api.js
|
||||
import store from '@/store'
|
||||
|
||||
const BASE_URL = 'http://49.233.115.141:11082'
|
||||
const TIMEOUT = 20000
|
||||
|
||||
/**
|
||||
* 统一请求封装
|
||||
* @param {string} method - GET | POST
|
||||
* @param {string} url - 接口路径
|
||||
* @param {object} data - 请求参数
|
||||
* @returns {Promise<{code, msg, data}>}
|
||||
*/
|
||||
export function request(method, url, data = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${store.token}`,
|
||||
'Userid': store.userId,
|
||||
'Username': store.userName
|
||||
}
|
||||
uni.request({
|
||||
url: BASE_URL + url,
|
||||
method,
|
||||
data: method === 'GET' ? undefined : data,
|
||||
// GET 参数通过 data 字段传递,UniApp 会自动拼接为 queryString
|
||||
...(method === 'GET' ? { data } : {}),
|
||||
header,
|
||||
timeout: TIMEOUT,
|
||||
success(res) {
|
||||
const { code, msg, data: resData } = res.data
|
||||
resolve({ code, msg, data: resData })
|
||||
},
|
||||
fail(err) {
|
||||
reject({ code: -1, msg: err.errMsg || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const get = (url, params) => request('GET', url, params)
|
||||
export const post = (url, data) => request('POST', url, data)
|
||||
6
odf-uniapp/services/auth.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { get, post } from './api'
|
||||
|
||||
export const appLogin = (username, password) => post('/appLogin', { username, password })
|
||||
export const checkPermission = () => get('/business/OdfPorts/odf')
|
||||
export const updatePassword = (oldPassword, newPassword) =>
|
||||
post('/system/user/profile/updateUserPwd', { oldPassword, newPassword })
|
||||
6
odf-uniapp/services/home.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { get } from './api'
|
||||
|
||||
export const getCompanyList = () => get('/business/OdfRooms/getcompany')
|
||||
export const getDictUnitType = () => get('/system/dict/data/type/odf_ports_unit_type')
|
||||
export const getDictBusinessType = () => get('/system/dict/data/type/odf_ports_business_type')
|
||||
export const checkAppVersion = (version) => get('/webapi/CheckAppVersion', { version })
|
||||
10
odf-uniapp/services/machine.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { get, post } from './api'
|
||||
|
||||
export const getRegionList = (deptId) => get('/business/OdfRooms/getregion', { deptId })
|
||||
export const getRoomList = (pageNum, pageSize, deptId) =>
|
||||
get('/business/OdfRooms/list', { pageNum, pageSize, deptId })
|
||||
export const getRackList = (pageNum, pageSize, roomId) =>
|
||||
get('/business/OdfRacks/list', { pageNum, pageSize, roomId })
|
||||
export const getRackDetail = (RackId) => get('/business/OdfPorts/mlist', { RackId })
|
||||
export const getPortDetail = (id) => get(`/business/OdfPorts/${id}`)
|
||||
export const savePort = (data) => post('/business/OdfPorts/save', data)
|
||||
4
odf-uniapp/services/search.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { get } from './api'
|
||||
|
||||
export const searchPorts = (key, pageNum, pageSize) =>
|
||||
get('/business/OdfPorts/search2', { key, pageNum, pageSize })
|
||||
BIN
odf-uniapp/static/images/home_bg.png
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
odf-uniapp/static/images/ic_back.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
odf-uniapp/static/images/ic_exit.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
odf-uniapp/static/images/ic_refresh.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
odf-uniapp/static/images/ic_search.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
odf-uniapp/static/images/ic_set.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
odf-uniapp/static/images/ic_update.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
odf-uniapp/static/images/login_bg.png
Normal file
|
After Width: | Height: | Size: 893 KiB |
BIN
odf-uniapp/static/logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
37
odf-uniapp/store/index.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// store/index.js
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const store = reactive({
|
||||
// 认证信息
|
||||
token: uni.getStorageSync('token') || '',
|
||||
userId: uni.getStorageSync('userId') || '',
|
||||
userName: uni.getStorageSync('userName') || '',
|
||||
isPermission: false,
|
||||
|
||||
// 字典数据
|
||||
dictUnitTypes: [], // 设备型号列表
|
||||
dictBusinessTypes: [], // 业务类型列表
|
||||
|
||||
// 设置认证信息
|
||||
setAuth(token, userId, userName) {
|
||||
this.token = token
|
||||
this.userId = userId
|
||||
this.userName = userName
|
||||
uni.setStorageSync('token', token)
|
||||
uni.setStorageSync('userId', userId)
|
||||
uni.setStorageSync('userName', userName)
|
||||
},
|
||||
|
||||
// 清除认证信息
|
||||
clearAuth() {
|
||||
this.token = ''
|
||||
this.userId = ''
|
||||
this.userName = ''
|
||||
this.isPermission = false
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userId')
|
||||
uni.removeStorageSync('userName')
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
13
odf-uniapp/uni.promisify.adaptor.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
uni.addInterceptor({
|
||||
returnValue (res) {
|
||||
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
|
||||
return res;
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
res.then((res) => {
|
||||
if (!res) return resolve(res)
|
||||
return res[0] ? reject(res[0]) : resolve(res[1])
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
88
odf-uniapp/uni.scss
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* ========== 绥时录自定义变量 ========== */
|
||||
$theme-color: #1A73EC;
|
||||
$bg-color-page: #F5F5F5;
|
||||
$bg-color-card: #FFFFFF;
|
||||
$text-color-primary: #333333;
|
||||
$text-color-secondary: #666666;
|
||||
$text-color-placeholder: #999999;
|
||||
$color-connected: #4CAF50;
|
||||
$color-disconnected: #F44336;
|
||||
$border-radius-card: 12rpx;
|
||||
$spacing-page: 24rpx;
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #1A73EC;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||