Browse Source
- 初始化项目结构,基于 UniApp + Vue 3 + TypeScript - 实现首页、闸口列表、数据上传、历史数据等功能 - 支持图片上传、定位获取、数据筛选等功能 - 使用 Pinia 状态管理和 uview-plus UI 组件库 - 添加示例数据便于查看效果 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>master
48 changed files with 14611 additions and 0 deletions
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
# CLAUDE.md |
||||
|
||||
此文件为 Claude Code (claude.ai/code) 在此代码库中工作时提供指导。 |
||||
|
||||
## 项目概述 |
||||
|
||||
**msmg-uni** 是一个基于 UniApp 框架构建的微信小程序移动应用,使用 Vue 3 + TypeScript + Vite + Unocss + uview-plus 技术栈。 |
||||
|
||||
## 编码风格 |
||||
|
||||
1. 尽可能使用if return 代替 if else |
||||
2. 代码结构清晰,关键地方需要中文注释 |
||||
3. 使用单引号替代双引号 |
||||
4. 函数参数尽量使用解构赋值 |
||||
5. 行结尾禁止添加分号 |
||||
|
||||
## 开发命令 |
||||
|
||||
### 开发环境 |
||||
- `npm run dev:mp-weixin` - 微信小程序开发 |
||||
|
||||
### 生产构建 |
||||
- `npm run build:mp-weixin` - 构建微信小程序 |
||||
|
||||
### 类型检查 |
||||
- `npm run type-check` - 运行 TypeScript 类型检查 |
||||
|
||||
## 架构设计 |
||||
|
||||
### 核心目录结构 |
||||
``` |
||||
src/ |
||||
├── @api/ # API 层和服务定义 |
||||
├── @layout/ # 布局组件 (PageTitle, RootView) |
||||
├── components/ # 可复用的 Vue 组件 |
||||
├── common/ # 全局工具函数和常量 |
||||
├── hooks/ # Vue 3 组合式函数 |
||||
├── pages/ # 应用页面 |
||||
├── stores/ # Pinia 状态管理 |
||||
├── static/ # 静态资源 |
||||
├── utils/ # 工具函数 |
||||
├── App.vue # 根应用组件 |
||||
├── main.ts # 应用入口文件 |
||||
├── pages.json # UniApp 页面配置 |
||||
└── manifest.json # 平台特定应用配置 |
||||
``` |
||||
|
||||
### 技术栈 |
||||
- **框架**: UniApp 3.x + Vue 3 组合式 API |
||||
- **语言**: TypeScript 4.9+ |
||||
- **构建工具**: Vite 5.2.8 |
||||
- **状态管理**: Pinia 2.0.36 |
||||
- **UI 库**: uview-plus 3.6.17 |
||||
- **样式**: UnoCSS + SCSS |
||||
- **HTTP**: 自定义工具 + 拦截器 |
||||
- **国际化**: vue-i18n |
||||
- **图标**: unocss icons tabler图标,使用i-tabler-* 引入图标 |
||||
|
||||
### 核心模式 |
||||
|
||||
**状态管理** |
||||
- 使用 `/src/stores/` 中的 Pinia stores |
||||
- 主题 store 支持明暗模式切换 |
||||
- 响应式状态的组合模式 |
||||
|
||||
**API 层** |
||||
- API 定义在 `/src/@api/` 中,使用 `.api.ts` 后缀 |
||||
- `/src/utils/http.ts` 中的 HTTP 工具处理: |
||||
- 请求/响应日志 |
||||
- `/types/` 中的 TypeScript DTO/VO 模式 |
||||
|
||||
**组件开发** |
||||
- 使用 Vue 3 组合式 API 和 `<script setup>` |
||||
- `/src/@layout/` 中的可复用布局组件 |
||||
- `/src/hooks/` 中的自定义组合函数 |
||||
- uview-plus 组件保证 UI 一致性 |
||||
|
||||
**样式** |
||||
- UnoCSS 用于原子化样式 |
||||
- 基于 CSS 变量的自定义主题系统 |
||||
- `/src/static/styles/theme.css` 中的全局样式 |
||||
- SCSS 支持,已屏蔽弃用警告 |
||||
|
||||
**类型安全** |
||||
- `/types/` 中的全面 TypeScript 定义 |
||||
- 分离的 DTO (数据传输对象) 和 VO (值对象) |
||||
- API 集成的 HTTP 响应类型 |
||||
- Vue 3 原生类型支持 |
||||
|
||||
## 跨平台开发 |
||||
|
||||
### 平台特定配置 |
||||
- `manifest.json` 包含平台特定应用配置 |
||||
- `pages.json` 定义页面路由和导航 |
||||
- 使用 `#ifdef` 和 `#ifndef` 条件编译 |
||||
|
||||
### 支持平台 |
||||
此应用支持部署到: |
||||
- 微信小程序 |
||||
|
||||
## 开发规范 |
||||
|
||||
|
||||
### Git 提交规范 |
||||
- 使用简洁明了的 commit 信息 |
||||
- 不要包含额外的开发者信息标注(如:🤖 Generated with [Claude Code]、Co-Authored-By 等) |
||||
- 保持纯粹的提交信息 |
||||
|
||||
### 文件命名 |
||||
- API 文件:`*.api.ts` 在 `/src/@api/` |
||||
- 组件:PascalCase (如 `PageBack.vue`) |
||||
- 组合函数:`*.use.ts` 在 `/src/hooks/` |
||||
- 状态管理:`*.store.ts` 在 `/src/stores/` |
||||
- 类型:按类别组织在 `/types/dto/` 和 `/types/vo/` |
||||
- 其他文件: 遵循`xxx-xxx.xxx`命名 |
||||
|
||||
### 导入别名 |
||||
- `@/` 指向 `./src/` (在 vite.config.ts 中配置) |
||||
- 使用绝对导入:`import { http } from '@/utils/http'` |
||||
|
||||
### 组件结构 |
||||
```vue |
||||
<template> |
||||
<!-- 使用 uview-plus 组件的模板 --> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
// TypeScript 组合式 API |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
// UnoCSS 工具类的作用域样式 |
||||
</style> |
||||
``` |
||||
|
||||
### 页面结构 |
||||
|
||||
根组件为<RootView /> |
||||
|
||||
### HTTP 请求 |
||||
使用配置的 HTTP 工具: |
||||
```typescript |
||||
import { http } from '@/utils/http' |
||||
``` |
||||
|
||||
### 状态管理 |
||||
创建带 TypeScript 的 Pinia stores: |
||||
```typescript |
||||
import { defineStore } from 'pinia' |
||||
|
||||
export const useExampleStore = defineStore('example', { |
||||
state: () => ({ |
||||
// 响应式状态 |
||||
}), |
||||
actions: { |
||||
// 操作方法 |
||||
} |
||||
}) |
||||
``` |
||||
|
||||
## 构建配置 |
||||
|
||||
### Vite 配置 |
||||
- UnoCSS 集成与微信小程序预设 |
||||
- SCSS 预处理 |
||||
- Vue JSX 支持 |
||||
- 配置路径别名 |
||||
|
||||
### UnoCSS 配置 |
||||
- 配置自定义主题颜色 |
||||
- Tabler 图标集成 |
||||
- 微信小程序兼容性 |
||||
- 定义的实用快捷方式 |
||||
|
||||
### TypeScript 配置 |
||||
- Vue 3 TypeScript 支持 |
||||
- UniApp 类型定义 |
||||
- 清理导入的路径映射 |
||||
- 启用严格模式 |
||||
|
||||
## API 文档查询 |
||||
|
||||
### Context7 集成 |
||||
可以使用 Context7 查询相关技术栈的最新 API 文档: |
||||
|
||||
|
||||
### 查询建议 |
||||
- 在开发过程中遇到 API 使用问题时,优先使用 Context7 查询最新文档 |
||||
- 可以指定 topic 参数来查询特定主题,例如:`topic="composition-api"` |
||||
- 对于具体的组件或功能,可以直接查询相关主题:`topic="u-button"`、`topic="router"` |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
{ |
||||
"permissions": { |
||||
"allow": [ |
||||
"mcp__filesystem__list_directory", |
||||
"mcp__filesystem__directory_tree", |
||||
"Bash(npx prettier:*)", |
||||
"Bash(npm run dev:*)", |
||||
"Bash(npm install:*)", |
||||
"Bash(pnpm install:*)", |
||||
"Bash(pnpm run dev:mp-weixin:*)", |
||||
"Bash(git add:*)" |
||||
], |
||||
"deny": [], |
||||
"ask": [] |
||||
} |
||||
} |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
.vscode |
||||
.idea |
||||
.local |
||||
index.html |
||||
!.env-config.ts |
||||
components.d.ts |
||||
/node_modules/ |
||||
/public/ |
||||
/dist/ |
||||
.hbuilderx/ |
||||
.DS_Store |
||||
.gitignore |
||||
.git/ |
||||
/.claude/ |
||||
@ -0,0 +1,177 @@
@@ -0,0 +1,177 @@
|
||||
module.exports = { |
||||
root: true, |
||||
env: { |
||||
browser: true, |
||||
// 必填
|
||||
node: true, |
||||
es2021: true |
||||
}, |
||||
parser: 'vue-eslint-parser', |
||||
extends: [ |
||||
'eslint:recommended', |
||||
'plugin:vue/vue3-recommended', |
||||
'plugin:@typescript-eslint/recommended', |
||||
'plugin:prettier/recommended', |
||||
// eslint-config-prettier 的缩写
|
||||
'prettier' |
||||
], |
||||
parserOptions: { |
||||
ecmaVersion: 12, |
||||
parser: '@typescript-eslint/parser', |
||||
sourceType: 'module', |
||||
ecmaFeatures: { |
||||
jsx: true |
||||
} |
||||
}, |
||||
// eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写
|
||||
plugins: ['vue', '@typescript-eslint', 'prettier'], |
||||
rules: { |
||||
'quote-props': [0], |
||||
// indentation (Already present in TypeScript)
|
||||
'comma-spacing': ['error', { before: false, after: true }], |
||||
'key-spacing': ['error', { afterColon: true }], |
||||
'n/prefer-global/process': ['off'], |
||||
'sonarjs/cognitive-complexity': ['off'], |
||||
'eol-last': 'off', |
||||
'antfu/top-level-function': 'off', |
||||
'@typescript-eslint/no-explicit-any': 'off', |
||||
|
||||
// indentation (Already present in TypeScript)
|
||||
indent: ['error', 2], |
||||
|
||||
// Enforce trailing comma (Already present in TypeScript)
|
||||
'comma-dangle': ['error', 'never'], |
||||
|
||||
// Enforce consistent spacing inside braces of object (Already present in TypeScript)
|
||||
'object-curly-spacing': ['error', 'always'], |
||||
|
||||
// Enforce camelCase naming convention
|
||||
camelcase: 'error', |
||||
|
||||
// Disable max-len
|
||||
'max-len': 'off', |
||||
|
||||
// 行结尾不使用分号
|
||||
semi: ['2', 'never'], |
||||
|
||||
// 使用单引号
|
||||
quotes: ['error', 'single', { avoidEscape: true }], |
||||
'promise/param-names': 'off', |
||||
'antfu/if-newline': 'off', |
||||
// add parens ony when required in arrow function
|
||||
'arrow-parens': ['error', 'as-needed'], |
||||
|
||||
// add new line above comment
|
||||
'newline-before-return': 0, |
||||
|
||||
// add new line above comment
|
||||
'lines-around-comment': [ |
||||
'off', |
||||
{ |
||||
beforeBlockComment: true, |
||||
beforeLineComment: true, |
||||
allowBlockStart: true, |
||||
allowClassStart: true, |
||||
allowObjectStart: true, |
||||
allowArrayStart: true, |
||||
|
||||
// We don't want to add extra space above closing SECTION
|
||||
ignorePattern: '!SECTION' |
||||
} |
||||
], |
||||
'@typescript-eslint/comma-dangle': ['error', 'never'], |
||||
|
||||
// Ignore _ as unused variable
|
||||
'@typescript-eslint/no-unused-vars': [0], |
||||
|
||||
'array-element-newline': ['error', 'consistent'], |
||||
'array-bracket-newline': ['error', 'consistent'], |
||||
'no-useless-catch': 'off', |
||||
'sonarjs/no-useless-catch': 'off', |
||||
'padding-line-between-statements': [ |
||||
0, |
||||
{ |
||||
blankLine: 'always', |
||||
prev: 'expression', |
||||
next: 'const' |
||||
}, |
||||
{ blankLine: 'always', prev: 'const', next: 'expression' }, |
||||
{ |
||||
blankLine: 'always', |
||||
prev: 'multiline-const', |
||||
next: '*' |
||||
}, |
||||
{ blankLine: 'always', prev: '*', next: 'multiline-const' } |
||||
], |
||||
|
||||
// Plugin: eslint-plugin-import
|
||||
'import/order': 'off', |
||||
'import/prefer-default-export': 'off', |
||||
'import/newline-after-import': ['error', { count: 1 }], |
||||
'no-restricted-imports': [ |
||||
0, |
||||
'vuetify/components', |
||||
{ |
||||
name: 'vue3-apexcharts', |
||||
message: 'apexcharts are autoimported' |
||||
} |
||||
], |
||||
|
||||
// For omitting extension for ts files
|
||||
'import/extensions': [ |
||||
'error', |
||||
'ignorePackages', |
||||
{ |
||||
js: 'never', |
||||
jsx: 'never', |
||||
ts: 'never', |
||||
tsx: 'never' |
||||
} |
||||
], |
||||
|
||||
// ignore virtual files
|
||||
'import/no-unresolved': [ |
||||
2, |
||||
{ |
||||
ignore: [ |
||||
'~pages$', |
||||
'virtual:generated-layouts', |
||||
'.*?css', |
||||
|
||||
// Ignore vite's ?raw imports
|
||||
'.*?raw', |
||||
// Ignore nuxt auth in nuxt version
|
||||
'#auth$' |
||||
] |
||||
} |
||||
], |
||||
|
||||
// Thanks: https://stackoverflow.com/a/63961972/10796681
|
||||
'no-shadow': 'off', |
||||
'@typescript-eslint/no-shadow': ['error'], |
||||
|
||||
'@typescript-eslint/consistent-type-imports': 'error', |
||||
'@typescript-eslint/no-explicit-any': 'off', |
||||
// Plugin: eslint-plugin-promise
|
||||
'promise/always-return': 'off', |
||||
'promise/catch-or-return': 'off', |
||||
|
||||
// -- Sonarlint
|
||||
'sonarjs/no-duplicate-string': 'off', |
||||
'sonarjs/no-nested-template-literals': 'off', |
||||
'no-undef': 'off', |
||||
'vue/multi-word-component-names': [ |
||||
'error', |
||||
{ |
||||
ignores: [] |
||||
} |
||||
], |
||||
'vue/v-on-event-hyphenation': 0 // html上的事件允许驼峰格式phoneCallback
|
||||
}, |
||||
globals: { |
||||
defineProps: 'readonly', |
||||
defineEmits: 'readonly', |
||||
defineExpose: 'readonly', |
||||
withDefaults: 'readonly' |
||||
} |
||||
} |
||||
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
# Logs |
||||
logs |
||||
*.log |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
pnpm-debug.log* |
||||
lerna-debug.log* |
||||
|
||||
node_modules |
||||
.DS_Store |
||||
dist |
||||
*.local |
||||
|
||||
# Editor directories and files |
||||
.idea |
||||
*.suo |
||||
*.ntvs* |
||||
*.njsproj |
||||
*.sln |
||||
*.sw? |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
# 依赖 |
||||
node_modules |
||||
pnpm-lock.yaml |
||||
package-lock.json |
||||
yarn.lock |
||||
|
||||
# 构建产物 |
||||
dist |
||||
build |
||||
.output |
||||
.nuxt |
||||
.next |
||||
.vite |
||||
|
||||
# 环境文件 |
||||
.env |
||||
.env.* |
||||
!.env.example |
||||
|
||||
# 日志文件 |
||||
*.log |
||||
logs |
||||
|
||||
# 临时文件 |
||||
*.tmp |
||||
*.temp |
||||
.cache |
||||
|
||||
# IDE 配置 |
||||
.vscode |
||||
.idea |
||||
*.swp |
||||
*.swo |
||||
|
||||
# 操作系统文件 |
||||
.DS_Store |
||||
Thumbs.db |
||||
|
||||
# 测试覆盖率 |
||||
coverage |
||||
|
||||
# TypeScript |
||||
*.d.ts |
||||
|
||||
# 微信小程序相关 |
||||
unpackage |
||||
miniprogram_npm |
||||
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
{ |
||||
"semi": false, |
||||
"singleQuote": true, |
||||
"quoteProps": "as-needed", |
||||
"trailingComma": "none", |
||||
"tabWidth": 2, |
||||
"useTabs": false, |
||||
"endOfLine": "lf", |
||||
"arrowParens": "avoid", |
||||
"bracketSpacing": true, |
||||
"htmlWhitespaceSensitivity": "css", |
||||
"vueIndentScriptAndStyle": false, |
||||
"printWidth": 80 |
||||
} |
||||
@ -0,0 +1,149 @@
@@ -0,0 +1,149 @@
|
||||
# 闸口数据管理小程序 |
||||
|
||||
基于 UniApp + Vue 3 + TypeScript 开发的微信小程序,用于水利、交通等场景的闸口监测管理。 |
||||
|
||||
## 功能特性 |
||||
|
||||
### 核心功能 |
||||
- ✅ **闸口数据上传**:支持图片上传、测流值记录、GPS定位 |
||||
- ✅ **历史数据查询**:数据列表展示、筛选查看、详情预览 |
||||
- ✅ **闸口站点管理**:多个站点切换、站点信息展示 |
||||
|
||||
### 技术实现 |
||||
- 🚀 **图片上传**:使用微信小程序 `wx.chooseImage` API,支持最多3张图片 |
||||
- 📍 **精准定位**:使用 `gcj02` 坐标系,支持高精度定位 |
||||
- 📊 **数据管理**:Pinia 状态管理,响应式数据更新 |
||||
- 🎨 **UI 组件**:UnoCSS 原子化样式 + uview-plus UI 组件 |
||||
|
||||
## 项目结构 |
||||
|
||||
``` |
||||
src/ |
||||
├── @api/ # API 接口定义 |
||||
├── @layout/ # 布局组件 |
||||
├── components/ # 通用组件 |
||||
├── common/ # 常量和工具函数 |
||||
├── hooks/ # 组合式函数 |
||||
├── pages/ # 页面文件 |
||||
│ ├── index/ # 首页(闸口选择) |
||||
│ └── gate/ # 闸口相关页面 |
||||
│ ├── list.vue # 闸口站点列表 |
||||
│ ├── upload.vue # 数据上传 |
||||
│ ├── history.vue # 历史数据 |
||||
│ └── detail.vue # 数据详情 |
||||
├── stores/ # 状态管理 |
||||
├── types/ # TypeScript 类型定义 |
||||
│ ├── dto/ # 数据传输对象 |
||||
│ └── vo/ # 值对象 |
||||
└── utils/ # 工具函数 |
||||
``` |
||||
|
||||
## 页面说明 |
||||
|
||||
### 1. 首页 (`pages/index/index.vue`) |
||||
- 闸口站点卡片展示 |
||||
- 快捷操作按钮 |
||||
- 支持站点选择和跳转 |
||||
|
||||
### 2. 数据上传 (`pages/gate/upload.vue`) |
||||
- **当前闸口信息**:显示选中站点,支持切换 |
||||
- **图片上传**:最多3张,支持预览和删除 |
||||
- **测流数据**:数字输入,单位 m³/s |
||||
- **定位信息**:自动获取当前位置,支持重新定位 |
||||
- **备注信息**:可选文字描述,最多200字 |
||||
- **数据验证**:确保必填项完整后提交 |
||||
|
||||
### 3. 历史数据 (`pages/gate/history.vue`) |
||||
- **筛选功能**:按闸口、时间筛选 |
||||
- **统计信息**:总记录数、今日记录数 |
||||
- **数据列表**:展示记录摘要,支持下拉刷新 |
||||
- **图片预览**:缩略图展示,支持点击预览 |
||||
|
||||
### 4. 数据详情 (`pages/gate/detail.vue`) |
||||
- **完整信息展示**:站点、时间、测流值、定位 |
||||
- **图片查看**:全屏预览,支持滑动切换 |
||||
- **地图定位**:点击位置信息打开微信内置地图 |
||||
|
||||
### 5. 闸口选择 (`pages/gate/list.vue`) |
||||
- **站点列表**:显示所有可用站点 |
||||
- **搜索功能**:按名称、地点搜索 |
||||
- **选中状态**:清晰标识当前选中站点 |
||||
|
||||
## 数据模型 |
||||
|
||||
### 闸口站点 |
||||
```typescript |
||||
interface GateStation { |
||||
id: string |
||||
name: string |
||||
location: string |
||||
coordinates: { |
||||
latitude: number |
||||
longitude: number |
||||
} |
||||
description: string |
||||
status: 'active' | 'inactive' | 'maintenance' |
||||
} |
||||
``` |
||||
|
||||
### 数据记录 |
||||
```typescript |
||||
interface GateDataRecordVO { |
||||
id: string |
||||
stationId: string |
||||
stationName: string |
||||
flowValue: number |
||||
images: string[] |
||||
location: Coordinates |
||||
remark?: string |
||||
createTime: string |
||||
status: 'pending' | 'approved' | 'rejected' |
||||
} |
||||
``` |
||||
|
||||
## 开发指南 |
||||
|
||||
### 运行项目 |
||||
```bash |
||||
# 安装依赖 |
||||
pnpm install |
||||
|
||||
# 微信小程序开发 |
||||
pnpm run dev:mp-weixin |
||||
``` |
||||
|
||||
### 技术栈 |
||||
- **框架**:UniApp 3.x + Vue 3 |
||||
- **语言**:TypeScript |
||||
- **构建**:Vite |
||||
- **状态管理**:Pinia |
||||
- **样式**:UnoCSS + SCSS |
||||
- **UI 组件**:uview-plus |
||||
- **图标**:Tabler Icons (UnoCSS) |
||||
|
||||
## 注意事项 |
||||
|
||||
### 权限管理 |
||||
- 位置权限:首次使用时需要用户授权 |
||||
- 相机权限:图片上传时需要用户授权 |
||||
|
||||
### 数据存储 |
||||
- 当前使用假数据进行演示 |
||||
- 实际项目中需要配置后端API接口 |
||||
|
||||
### 性能优化 |
||||
- 图片支持压缩上传 |
||||
- 列表数据分页加载 |
||||
- 定位信息适当缓存 |
||||
|
||||
## 扩展功能建议 |
||||
|
||||
1. **数据导出**:支持Excel导出历史数据 |
||||
2. **数据可视化**:集成图表展示流量变化趋势 |
||||
3. **离线模式**:支持离线数据存储和同步 |
||||
4. **消息推送**:异常情况告警通知 |
||||
5. **权限管理**:多角色用户权限控制 |
||||
|
||||
## License |
||||
|
||||
MIT License |
||||
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html> |
||||
<html> |
||||
<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="/src/main.ts"></script> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
{ |
||||
"name": "msmg-uni", |
||||
"version": "0.0.0", |
||||
"scripts": { |
||||
"dev:custom": "uni -p", |
||||
"dev:h5": "uni", |
||||
"dev:h5:ssr": "uni --ssr", |
||||
"dev:mp-alipay": "uni -p mp-alipay", |
||||
"dev:mp-baidu": "uni -p mp-baidu", |
||||
"dev:mp-jd": "uni -p mp-jd", |
||||
"dev:mp-kuaishou": "uni -p mp-kuaishou", |
||||
"dev:mp-lark": "uni -p mp-lark", |
||||
"dev:mp-qq": "uni -p mp-qq", |
||||
"dev:mp-toutiao": "uni -p mp-toutiao", |
||||
"dev:mp-harmony": "uni -p mp-harmony", |
||||
"dev:mp-weixin": "uni -p mp-weixin", |
||||
"dev:mp-xhs": "uni -p mp-xhs", |
||||
"dev:quickapp-webview": "uni -p quickapp-webview", |
||||
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei", |
||||
"dev:quickapp-webview-union": "uni -p quickapp-webview-union", |
||||
"build:custom": "uni build -p", |
||||
"build:h5": "uni build", |
||||
"build:h5:ssr": "uni build --ssr", |
||||
"build:mp-alipay": "uni build -p mp-alipay", |
||||
"build:mp-baidu": "uni build -p mp-baidu", |
||||
"build:mp-jd": "uni build -p mp-jd", |
||||
"build:mp-kuaishou": "uni build -p mp-kuaishou", |
||||
"build:mp-lark": "uni build -p mp-lark", |
||||
"build:mp-qq": "uni build -p mp-qq", |
||||
"build:mp-toutiao": "uni build -p mp-toutiao", |
||||
"build:mp-harmony": "uni build -p mp-harmony", |
||||
"build:mp-weixin": "uni build -p mp-weixin", |
||||
"build:mp-xhs": "uni build -p mp-xhs", |
||||
"build:quickapp-webview": "uni build -p quickapp-webview", |
||||
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", |
||||
"build:quickapp-webview-union": "uni build -p quickapp-webview-union", |
||||
"type-check": "vue-tsc --noEmit" |
||||
}, |
||||
"dependencies": { |
||||
"@dcloudio/uni-app": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-app-harmony": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-app-plus": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-components": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-h5": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-alipay": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-baidu": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-harmony": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-jd": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-lark": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-qq": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-toutiao": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-weixin": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-mp-xhs": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4080520251106001", |
||||
"clipboard": "^2.0.11", |
||||
"pinia": "2.0.36", |
||||
"uview-plus": "^3.6.17", |
||||
"vue": "^3.4.21", |
||||
"vue-i18n": "^9.1.9" |
||||
}, |
||||
"devDependencies": { |
||||
"@dcloudio/types": "^3.4.8", |
||||
"@dcloudio/uni-automator": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-cli-shared": "3.0.0-4080520251106001", |
||||
"@dcloudio/uni-stacktracey": "3.0.0-4080520251106001", |
||||
"@dcloudio/vite-plugin-uni": "3.0.0-4080520251106001", |
||||
"@iconify-json/streamline-color": "^1.2.2", |
||||
"@iconify-json/tabler": "^1.2.17", |
||||
"@vitejs/plugin-vue-jsx": "^4.1.2", |
||||
"@vue/eslint-config-prettier": "^10.2.0", |
||||
"@vue/runtime-core": "^3.4.21", |
||||
"@vue/tsconfig": "^0.1.3", |
||||
"class-variance-authority": "^0.7.1", |
||||
"eslint": "^9.24.0", |
||||
"eslint-plugin-prettier": "^5.2.6", |
||||
"eslint-plugin-vue": "^10.0.0", |
||||
"prettier": "^3.5.3", |
||||
"sass": "1.63.2", |
||||
"sass-loader": "10.4.1", |
||||
"typescript": "^4.9.4", |
||||
"unocss": "^0.65.4", |
||||
"unocss-preset-weapp": "^0.65.0", |
||||
"uuid": "^11.1.0", |
||||
"vite": "5.2.8", |
||||
"vue-tsc": "^1.0.24" |
||||
} |
||||
} |
||||
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
/// <reference types='@dcloudio/types' />
|
||||
import 'vue' |
||||
|
||||
declare module '@vue/runtime-core' { |
||||
type Hooks = App.AppInstance & Page.PageInstance; |
||||
|
||||
interface ComponentCustomOptions extends Hooks { |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
/** |
||||
* 用户相关接口 |
||||
*/ |
||||
import { baseRequestIns as request } from '@/utils/http' |
||||
|
||||
export const getUserInfoHttp = ( |
||||
data: SignDto.LoginDto |
||||
): Http.IRes<SignVo.LoginVo> => |
||||
request({ |
||||
data, |
||||
method: 'POST', |
||||
url: '/sign/login' |
||||
}) |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
<template> |
||||
<view class="page-title"> |
||||
<StatusBar /> |
||||
<view class="title-spacer"></view> |
||||
<view class="title-content"> |
||||
<PageBack class="title-back" :title="title || ''" /> |
||||
<view class="title-center"> |
||||
<slot></slot> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import StatusBar from '@/components/StatusBar.vue' |
||||
import PageBack from '@/components/PageBack.vue' |
||||
|
||||
const props = defineProps<{ |
||||
title?: string |
||||
}>() |
||||
</script> |
||||
|
||||
<style scoped> |
||||
.page-title { |
||||
background-color: white; |
||||
border-bottom: 2rpx solid #e5e7eb; |
||||
} |
||||
|
||||
.title-spacer { |
||||
height: 50rpx; |
||||
} |
||||
|
||||
.title-content { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
height: 88rpx; |
||||
padding: 0 32rpx; |
||||
} |
||||
|
||||
.title-center { |
||||
flex: 1; |
||||
text-align: center; |
||||
font-size: 36rpx; |
||||
font-weight: 600; |
||||
color: #333; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
<template> |
||||
<view class="root-view"> |
||||
<slot /> |
||||
</view> |
||||
</template> |
||||
|
||||
<script lang="ts" setup> |
||||
// 不需要复杂的逻辑,简化组件 |
||||
</script> |
||||
|
||||
<style> |
||||
.root-view { |
||||
min-height: 100vh; |
||||
background-color: #f5f5f5; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts"> |
||||
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app"; |
||||
onLaunch(() => { |
||||
console.log("App Launch"); |
||||
}); |
||||
onShow(() => { |
||||
console.log("App Show"); |
||||
}); |
||||
onHide(() => { |
||||
console.log("App Hide"); |
||||
}); |
||||
</script> |
||||
<style lang="scss"> |
||||
@import "uview-plus/index.scss"; |
||||
@import "./static/styles/theme.css"; |
||||
|
||||
/* 防止iOS键盘弹出时页面滚动 */ |
||||
page { |
||||
height: 100%; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
</style> |
||||
@ -0,0 +1,153 @@
@@ -0,0 +1,153 @@
|
||||
// 闸口数据常量和假数据
|
||||
|
||||
import type { GateStation } from '../types/dto/gate.dto' |
||||
|
||||
// 闸口站点假数据
|
||||
export const gateStations: GateStation[] = [ |
||||
{ |
||||
id: 'GZ001', |
||||
name: '广州天河闸口', |
||||
location: '广东省广州市天河区珠江新城', |
||||
coordinates: { |
||||
latitude: 23.1291, |
||||
longitude: 113.2644 |
||||
}, |
||||
description: '主要防洪闸口,监控珠江水位', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'SZ002', |
||||
name: '深圳南山闸口', |
||||
location: '广东省深圳市南山区科技园', |
||||
coordinates: { |
||||
latitude: 22.5431, |
||||
longitude: 113.9491 |
||||
}, |
||||
description: '城市内涝监测点,智能调控系统', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'DG003', |
||||
name: '东莞东城闸口', |
||||
location: '广东省东莞市东城区东莞大道', |
||||
coordinates: { |
||||
latitude: 23.0489, |
||||
longitude: 113.7447 |
||||
}, |
||||
description: '东莞主要水闸,防潮排涝双功能', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'FS004', |
||||
name: '佛山禅城闸口', |
||||
location: '广东省佛山市禅城区祖庙街道', |
||||
coordinates: { |
||||
latitude: 23.0217, |
||||
longitude: 113.1219 |
||||
}, |
||||
description: '老城区防洪设施,保护历史建筑', |
||||
status: 'maintenance' |
||||
}, |
||||
{ |
||||
id: 'ZH005', |
||||
name: '珠海香洲闸口', |
||||
location: '广东省珠海市香洲区情侣路', |
||||
coordinates: { |
||||
latitude: 22.2769, |
||||
longitude: 113.5767 |
||||
}, |
||||
description: '沿海防潮闸,抵御台风海潮', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'HZ006', |
||||
name: '惠州惠城闸口', |
||||
location: '广东省惠州市惠城区江北街道', |
||||
coordinates: { |
||||
latitude: 23.0746, |
||||
longitude: 114.4136 |
||||
}, |
||||
description: '东江水利枢纽,调节水位流量', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'JM007', |
||||
name: '江门蓬江闸口', |
||||
location: '广东省江门市蓬江区港口路', |
||||
coordinates: { |
||||
latitude: 22.5802, |
||||
longitude: 113.0946 |
||||
}, |
||||
description: '西江支流控制闸,防汛抗旱', |
||||
status: 'inactive' |
||||
}, |
||||
{ |
||||
id: 'ZS008', |
||||
name: '中山石岐闸口', |
||||
location: '广东省中山市石岐区兴中道', |
||||
coordinates: { |
||||
latitude: 22.5211, |
||||
longitude: 113.3825 |
||||
}, |
||||
description: '城市排水系统核心,智能管理', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'ST009', |
||||
name: '汕头龙湖闸口', |
||||
location: '广东省汕头市龙湖区珠江路', |
||||
coordinates: { |
||||
latitude: 23.3924, |
||||
longitude: 116.7081 |
||||
}, |
||||
description: '韩江出海口,防潮防台', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'HY010', |
||||
name: '河源源城闸口', |
||||
location: '广东省河源市源城区建设大道', |
||||
coordinates: { |
||||
latitude: 23.7396, |
||||
longitude: 114.6974 |
||||
}, |
||||
description: '新丰江水库出水口,水力发电', |
||||
status: 'maintenance' |
||||
}, |
||||
{ |
||||
id: 'QY011', |
||||
name: '清远清城闸口', |
||||
location: '广东省清远市清城区北江路', |
||||
coordinates: { |
||||
latitude: 23.6817, |
||||
longitude: 113.0369 |
||||
}, |
||||
description: '北江中游枢纽,防洪灌溉', |
||||
status: 'active' |
||||
}, |
||||
{ |
||||
id: 'ZJ012', |
||||
name: '湛江赤坎闸口', |
||||
location: '广东省湛江市赤坎区中山一路', |
||||
coordinates: { |
||||
latitude: 21.2661, |
||||
longitude: 110.3659 |
||||
}, |
||||
description: '雷州半岛水利枢纽,防咸蓄淡', |
||||
status: 'active' |
||||
} |
||||
] |
||||
|
||||
// 闸口状态映射
|
||||
export const gateStatusMap = { |
||||
active: '运行中', |
||||
inactive: '停用', |
||||
maintenance: '维护中' |
||||
} as const |
||||
|
||||
// 闸口数据状态映射
|
||||
export const dataStatusMap = { |
||||
pending: '待审核', |
||||
approved: '已通过', |
||||
rejected: '已拒绝' |
||||
} as const |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
export const APP_NAME = 'msmg-uni' |
||||
export const APP_VERSION = 'v1.0.0' |
||||
export const APP_ICP = '' |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
<template> |
||||
<view class="inline-flex items-center text-5 duration-200 active:opacity-40"> |
||||
<view class="inline-flex items-center justify-center" @click="handleBack"> |
||||
<view :class="`p2 ${title ? 'pr-0' : ''}`"> |
||||
<view class="i-tabler:chevron-left"></view> |
||||
</view> |
||||
<view class="pr-4 font-600 flex items-center" v-if="title"> |
||||
<text class="text-4">{{ title }}</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
const props = withDefaults( |
||||
defineProps<{ |
||||
url?: string |
||||
title?: string |
||||
}>(), |
||||
{ |
||||
title: '' |
||||
} |
||||
) |
||||
|
||||
const handleBack = () => { |
||||
if (props.url) { |
||||
uni.navigateTo({ url: props.url }) |
||||
return |
||||
} |
||||
uni.navigateBack() |
||||
} |
||||
</script> |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
<template> |
||||
<view class="status-bar"></view> |
||||
</template> |
||||
<script setup lang="ts"></script> |
||||
<style scoped> |
||||
.status-bar { |
||||
height: var(--status-bar-height); |
||||
} |
||||
</style> |
||||
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' { |
||||
import { DefineComponent } from 'vue' |
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any> |
||||
export default component |
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
export const useToast = (content: string) => { |
||||
if (!content) return |
||||
uni.showToast({ |
||||
title: content, |
||||
icon: 'none' |
||||
}) |
||||
} |
||||
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
import { createSSRApp } from 'vue' |
||||
import 'virtual:uno.css' |
||||
import uviewPlus from 'uview-plus' |
||||
import pinia from './stores' |
||||
import App from './App.vue' |
||||
export function createApp() { |
||||
const app = createSSRApp(App) |
||||
app.use(uviewPlus) |
||||
app.use(pinia) |
||||
return { |
||||
app |
||||
} |
||||
} |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
{ |
||||
"name": "msmg-uni", |
||||
"appid": "__UNI__95CEB3C", |
||||
"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, |
||||
"mergeVirtualHostAttributes": true |
||||
}, |
||||
"mp-alipay": { |
||||
"usingComponents": true, |
||||
"mergeVirtualHostAttributes" : true |
||||
}, |
||||
"mp-baidu": { |
||||
"usingComponents": true, |
||||
"mergeVirtualHostAttributes" : true |
||||
}, |
||||
"mp-toutiao": { |
||||
"usingComponents": true, |
||||
"mergeVirtualHostAttributes" : true |
||||
}, |
||||
"uniStatistics": { |
||||
"enable": false |
||||
}, |
||||
"vueVersion": "3" |
||||
} |
||||
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
{ |
||||
"easycom": { |
||||
"autoscan": true, |
||||
"custom": { |
||||
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue", |
||||
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue", |
||||
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue" |
||||
} |
||||
}, |
||||
"pages": [ |
||||
{ |
||||
"path": "pages/index/index", |
||||
"style": { |
||||
"navigationBarTitleText": "闸口数据管理", |
||||
"navigationBarBackgroundColor": "#667eea", |
||||
"navigationBarTextStyle": "white" |
||||
} |
||||
}, |
||||
{ |
||||
"path": "pages/gate/list", |
||||
"style": { |
||||
"navigationBarTitleText": "闸口选择", |
||||
"navigationBarBackgroundColor": "#667eea", |
||||
"navigationBarTextStyle": "white" |
||||
} |
||||
}, |
||||
{ |
||||
"path": "pages/gate/upload", |
||||
"style": { |
||||
"navigationBarTitleText": "数据上传", |
||||
"navigationBarBackgroundColor": "#667eea", |
||||
"navigationBarTextStyle": "white" |
||||
} |
||||
}, |
||||
{ |
||||
"path": "pages/gate/history", |
||||
"style": { |
||||
"navigationBarTitleText": "历史数据", |
||||
"navigationBarBackgroundColor": "#667eea", |
||||
"navigationBarTextStyle": "white", |
||||
"enablePullDownRefresh": true |
||||
} |
||||
}, |
||||
{ |
||||
"path": "pages/gate/detail", |
||||
"style": { |
||||
"navigationBarTitleText": "数据详情", |
||||
"navigationBarBackgroundColor": "#667eea", |
||||
"navigationBarTextStyle": "white" |
||||
} |
||||
} |
||||
], |
||||
"globalStyle": { |
||||
"navigationBarBackgroundColor": "#667eea", |
||||
"navigationBarTextStyle": "white", |
||||
"backgroundColor": "#f5f5f5" |
||||
}, |
||||
} |
||||
@ -0,0 +1,260 @@
@@ -0,0 +1,260 @@
|
||||
<template> |
||||
<RootView> |
||||
<PageTitle title="数据详情" /> |
||||
<view v-if="record" class="min-h-screen bg-gray-50"> |
||||
<!-- 顶部信息卡片 --> |
||||
<view class="bg-gradient-to-r from-blue-500 to-blue-600 px-4 pt-4 pb-6"> |
||||
<view class="text-white"> |
||||
<view class="flex items-center justify-between mb-2"> |
||||
<view> |
||||
<text class="text-2xl font-bold">{{ record.stationName }}</text> |
||||
<text class="text-sm opacity-90 block mt-1">{{ record.location }}</text> |
||||
</view> |
||||
<view |
||||
class="px-3 py-1 bg-white bg-opacity-20 rd-full text-sm font-medium" |
||||
> |
||||
{{ dataStatusMap[record.status] }} |
||||
</view> |
||||
</view> |
||||
<view class="flex items-center mt-4 text-sm opacity-90"> |
||||
<view class="i-tabler-clock mr-2"></view> |
||||
<text>{{ formatDateTime(record.createTime) }}</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 测流数据 --> |
||||
<view class="px-4 -mt-3"> |
||||
<view class="bg-white rd-6 p-6 shadow-sm"> |
||||
<view class="flex items-center mb-4"> |
||||
<view class="w-8 h-8 bg-blue-100 rd-lg flex-center mr-3"> |
||||
<view class="i-tabler-droplet text-blue-600 text-lg"></view> |
||||
</view> |
||||
<text class="text-lg font-semibold text-text">测流数据</text> |
||||
</view> |
||||
<view class="bg-gradient-to-r from-blue-50 to-blue-100 rd-4 p-6 text-center"> |
||||
<text class="text-5xl font-bold text-blue-600">{{ record.flowValue }}</text> |
||||
<text class="text-xl text-blue-500 ml-2">m³/s</text> |
||||
<view class="mt-3 text-sm text-blue-600">实时流量监测值</view> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 定位信息 --> |
||||
<view class="px-4 mb-4"> |
||||
<view class="bg-white rd-6 p-6 shadow-sm"> |
||||
<view class="flex items-center mb-4"> |
||||
<view class="w-8 h-8 bg-green-100 rd-lg flex-center mr-3"> |
||||
<view class="i-tabler-map-pin text-green-600 text-lg"></view> |
||||
</view> |
||||
<text class="text-lg font-semibold text-text">定位信息</text> |
||||
</view> |
||||
<view class="grid grid-cols-2 gap-4 mb-4"> |
||||
<view class="bg-gray-50 rd-lg p-4"> |
||||
<text class="text-xs text-gray-500 block mb-1">纬度</text> |
||||
<text class="text-base font-medium text-gray-800">{{ record.location.latitude.toFixed(6) }}°</text> |
||||
</view> |
||||
<view class="bg-gray-50 rd-lg p-4"> |
||||
<text class="text-xs text-gray-500 block mb-1">经度</text> |
||||
<text class="text-base font-medium text-gray-800">{{ record.location.longitude.toFixed(6) }}°</text> |
||||
</view> |
||||
</view> |
||||
<button |
||||
class="w-full bg-green-500 text-white py-3 rd-lg font-medium active:scale-95 transition-transform" |
||||
@click="openMap" |
||||
> |
||||
<view class="i-tabler-map mr-2"></view> |
||||
查看地图位置 |
||||
</button> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 现场照片 --> |
||||
<view v-if="record.images.length > 0" class="px-4 mb-4"> |
||||
<view class="bg-white rd-6 p-6 shadow-sm"> |
||||
<view class="flex items-center justify-between mb-4"> |
||||
<view class="flex items-center"> |
||||
<view class="w-8 h-8 bg-purple-100 rd-lg flex-center mr-3"> |
||||
<view class="i-tabler-photo text-purple-600 text-lg"></view> |
||||
</view> |
||||
<text class="text-lg font-semibold text-text">现场照片</text> |
||||
</view> |
||||
<text class="text-sm text-gray-500">{{ record.images.length }}张</text> |
||||
</view> |
||||
<view class="grid grid-cols-3 gap-4"> |
||||
<view |
||||
v-for="(image, index) in record.images" |
||||
:key="index" |
||||
class="rd-2 overflow-hidden bg-gray-100 shadow-sm active:scale-95 transition-transform" |
||||
style="padding-top: 100%" |
||||
@click="previewImage(image, record.images, index)" |
||||
> |
||||
<image |
||||
:src="image" |
||||
mode="aspectFill" |
||||
class="absolute inset-0 w-full h-full" |
||||
></image> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 备注信息 --> |
||||
<view v-if="record.remark" class="bg-white rd-4 p-4 mb-4 shadow-sm"> |
||||
<text class="font-bold text-text mb-3 block">备注信息</text> |
||||
<text class="text-sm text-text2 leading-relaxed">{{ record.remark }}</text> |
||||
</view> |
||||
|
||||
<!-- 上传者信息 --> |
||||
<view v-if="uploader" class="bg-white rd-4 p-4 shadow-sm"> |
||||
<text class="font-bold text-text mb-3 block">上传信息</text> |
||||
<view class="flex items-center"> |
||||
<u-avatar |
||||
:src="uploader.avatar" |
||||
:text="uploader.name" |
||||
size="40" |
||||
/> |
||||
<view class="ml-3"> |
||||
<text class="text-sm font-medium text-text">{{ uploader.name }}</text> |
||||
<text class="text-xs text-text2 block">ID: {{ uploader.id }}</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 数据不存在 --> |
||||
<view v-else class="flex-center h-full"> |
||||
<view class="text-center"> |
||||
<view class="i-tabler-database-off text-6xl text-gray-300 mb-4"></view> |
||||
<text class="text-gray-500">数据不存在或已被删除</text> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- Toast提示 --> |
||||
<u-toast ref="uToast" /> |
||||
</RootView> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref, computed, onMounted } from 'vue' |
||||
import PageTitle from '@/@layout/PageTitle.vue' |
||||
import RootView from '@/@layout/RootView.vue' |
||||
import { useGateStore } from '@/stores/gate.store' |
||||
import { dataStatusMap } from '@/common/gate-data' |
||||
import type { GateDataRecordVO } from '../../../types/vo/gate.vo' |
||||
|
||||
const gateStore = useGateStore() |
||||
|
||||
// 响应式数据 |
||||
const recordId = ref('') |
||||
const uToast = ref() |
||||
|
||||
// 模拟数据 |
||||
const record = ref<GateDataRecordVO | null>(null) |
||||
const uploader = ref({ |
||||
id: 'user001', |
||||
name: '张三', |
||||
avatar: '' |
||||
}) |
||||
|
||||
// 获取状态样式 |
||||
const getStatusClass = (status: string) => { |
||||
switch (status) { |
||||
case 'approved': |
||||
return 'bg-green-100 text-green-600' |
||||
case 'pending': |
||||
return 'bg-yellow-100 text-yellow-600' |
||||
case 'rejected': |
||||
return 'bg-red-100 text-red-600' |
||||
default: |
||||
return 'bg-gray-100 text-gray-600' |
||||
} |
||||
} |
||||
|
||||
// 格式化日期时间 |
||||
const formatDateTime = (time: string) => { |
||||
const date = new Date(time) |
||||
return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}` |
||||
} |
||||
|
||||
// 预览图片 |
||||
const previewImage = (current: string, urls: string[], index: number) => { |
||||
gateStore.previewImage(current, urls) |
||||
} |
||||
|
||||
// 打开地图 |
||||
const openMap = () => { |
||||
if (!record.value) return |
||||
|
||||
const { latitude, longitude } = record.value.location |
||||
|
||||
// 使用微信内置地图查看位置 |
||||
uni.openLocation({ |
||||
latitude, |
||||
longitude, |
||||
name: record.value.stationName, |
||||
address: record.value.location || '测流点位置', |
||||
success: () => { |
||||
console.log('打开地图成功') |
||||
}, |
||||
fail: (error) => { |
||||
console.error('打开地图失败:', error) |
||||
uToast.value.show({ |
||||
type: 'error', |
||||
message: '打开地图失败' |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 获取数据详情 |
||||
const fetchRecordDetail = async (id: string) => { |
||||
try { |
||||
// 从store中查找记录 |
||||
const found = gateStore.historyData.find(item => item.id === id) |
||||
|
||||
if (found) { |
||||
record.value = found |
||||
} else { |
||||
// 如果store中没有,这里应该调用API获取详情 |
||||
// 暂时使用模拟数据 |
||||
record.value = { |
||||
id, |
||||
stationId: 'GZ001', |
||||
stationName: '广州天河闸口', |
||||
flowValue: 125.5, |
||||
images: [ |
||||
'https://picsum.photos/300/300?random=1', |
||||
'https://picsum.photos/300/300?random=2' |
||||
], |
||||
location: { latitude: 23.1291, longitude: 113.2644 }, |
||||
remark: '今日水量较大,需要密切关注水位变化。现场设备运行正常。', |
||||
createTime: new Date().toISOString(), |
||||
status: 'approved' |
||||
} |
||||
} |
||||
} catch (error) { |
||||
uToast.value.show({ |
||||
type: 'error', |
||||
message: '获取数据详情失败' |
||||
}) |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
// 获取页面参数 |
||||
const pages = getCurrentPages() |
||||
const currentPage = pages[pages.length - 1] |
||||
const options = currentPage.options as any |
||||
|
||||
if (options.id) { |
||||
recordId.value = options.id |
||||
fetchRecordDetail(options.id) |
||||
} else { |
||||
uToast.value.show({ |
||||
type: 'error', |
||||
message: '缺少数据ID' |
||||
}) |
||||
} |
||||
}) |
||||
</script> |
||||
@ -0,0 +1,487 @@
@@ -0,0 +1,487 @@
|
||||
<template> |
||||
<RootView> |
||||
<view class="page-container"> |
||||
<!-- 顶部标题栏 --> |
||||
<view class="header"> |
||||
<text class="title">历史数据</text> |
||||
</view> |
||||
|
||||
<!-- 筛选栏 --> |
||||
<view class="filter-bar"> |
||||
<view class="filter-item" @click="handleStationFilter"> |
||||
<i class="i-tabler-building-factory-2 filter-icon"></i> |
||||
<text class="filter-text">{{ selectedStationName }}</text> |
||||
<i class="i-tabler-chevron-down filter-arrow"></i> |
||||
</view> |
||||
<view class="filter-item" @click="handleDateFilter"> |
||||
<i class="i-tabler-calendar filter-icon"></i> |
||||
<text class="filter-text">{{ dateRangeText }}</text> |
||||
<i class="i-tabler-chevron-down filter-arrow"></i> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 统计卡片 --> |
||||
<view class="stats-container"> |
||||
<view class="stats-row"> |
||||
<view class="stat-item"> |
||||
<text class="stat-value">{{ stats.totalRecords }}</text> |
||||
<text class="stat-label">总记录</text> |
||||
</view> |
||||
<view class="stat-divider"></view> |
||||
<view class="stat-item"> |
||||
<text class="stat-value">{{ stats.todayRecords }}</text> |
||||
<text class="stat-label">今日记录</text> |
||||
</view> |
||||
<view class="stat-divider"></view> |
||||
<view class="stat-item"> |
||||
<text class="stat-value">125.5</text> |
||||
<text class="stat-label">平均流量</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 数据列表 --> |
||||
<scroll-view class="data-list-container" scroll-y> |
||||
<view v-if="historyData.length > 0" class="data-list"> |
||||
<view |
||||
v-for="record in historyData" |
||||
:key="record.id" |
||||
class="data-card" |
||||
@click="viewDetail(record)" |
||||
> |
||||
<!-- 卡片头部 --> |
||||
<view class="card-header"> |
||||
<view class="station-info"> |
||||
<text class="station-name">{{ record.stationName }}</text> |
||||
<text class="record-time">{{ formatTime(record.createTime) }}</text> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 数据内容 --> |
||||
<view class="card-content"> |
||||
<view class="data-row"> |
||||
<view class="data-item"> |
||||
<i class="i-tabler-droplet data-icon blue"></i> |
||||
<text class="data-value">{{ record.flowValue }}</text> |
||||
<text class="data-unit">m³/s</text> |
||||
</view> |
||||
<view class="data-item"> |
||||
<i class="i-tabler-map-pin data-icon green"></i> |
||||
<text class="data-label">已定位</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 图片预览 --> |
||||
<view v-if="record.images.length > 0" class="image-preview"> |
||||
<view |
||||
v-for="(image, imgIndex) in record.images.slice(0, 3)" |
||||
:key="imgIndex" |
||||
class="preview-item" |
||||
@click.stop="previewImages(record.images, imgIndex)" |
||||
> |
||||
<image :src="image" mode="aspectFill" class="preview-img" /> |
||||
</view> |
||||
<view |
||||
v-if="record.images.length > 3" |
||||
class="more-images" |
||||
@click.stop="previewImages(record.images, 0)" |
||||
> |
||||
<text class="more-text">+{{ record.images.length - 3 }}</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 空状态 --> |
||||
<view v-else class="empty-state"> |
||||
<i class="i-tabler-database-off empty-icon"></i> |
||||
<text class="empty-text">暂无历史数据</text> |
||||
</view> |
||||
</scroll-view> |
||||
</view> |
||||
|
||||
<!-- Toast提示 --> |
||||
<u-toast ref="uToast" /> |
||||
</RootView> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref, computed, onMounted } from 'vue' |
||||
import RootView from '@/@layout/RootView.vue' |
||||
import { useGateStore } from '@/stores/gate.store' |
||||
import { gateStations } from '@/common/gate-data' |
||||
import type { GateDataRecordVO } from '../../../types/vo/gate.vo' |
||||
import type { GateStation } from '../../../types/dto/gate.dto' |
||||
|
||||
const gateStore = useGateStore() |
||||
|
||||
// 响应式数据 |
||||
const selectedStation = ref<GateStation | null>(null) |
||||
const selectedDate = ref(new Date()) |
||||
const uToast = ref() |
||||
|
||||
// 计算属性 |
||||
const historyData = computed(() => { |
||||
console.log('historyData computed:', gateStore.historyData.length, '条记录') |
||||
return gateStore.historyData |
||||
}) |
||||
|
||||
const selectedStationName = computed(() => { |
||||
return selectedStation.value?.name || '全部闸口' |
||||
}) |
||||
|
||||
const dateRangeText = computed(() => { |
||||
return selectedDate.value ? formatDate(selectedDate.value) : '选择日期' |
||||
}) |
||||
|
||||
const stats = computed(() => { |
||||
const today = new Date().toDateString() |
||||
const todayRecords = historyData.value.filter(record => { |
||||
return new Date(record.createTime).toDateString() === today |
||||
}) |
||||
|
||||
return { |
||||
totalRecords: historyData.value.length, |
||||
todayRecords: todayRecords.length, |
||||
avgFlowValue: 0, |
||||
maxFlowValue: 0, |
||||
minFlowValue: 0 |
||||
} |
||||
}) |
||||
|
||||
|
||||
// 格式化时间 |
||||
const formatTime = (time: string) => { |
||||
const date = new Date(time) |
||||
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}` |
||||
} |
||||
|
||||
// 格式化日期 |
||||
const formatDate = (date: Date) => { |
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` |
||||
} |
||||
|
||||
// 选择闸口 |
||||
const handleStationFilter = () => { |
||||
uni.showActionSheet({ |
||||
itemList: ['全部闸口', ...gateStations.map(s => s.name)], |
||||
success: (res: any) => { |
||||
if (res.tapIndex === 0) { |
||||
selectedStation.value = null |
||||
} else { |
||||
selectedStation.value = gateStations[res.tapIndex - 1] |
||||
} |
||||
// 刷新数据 |
||||
gateStore.getHistoryData(selectedStation.value?.id) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
// 选择日期 |
||||
const handleDateFilter = () => { |
||||
uni.showModal({ |
||||
title: '提示', |
||||
content: '日期筛选功能开发中,敬请期待', |
||||
showCancel: false |
||||
}) |
||||
} |
||||
|
||||
// 查看详情 |
||||
const viewDetail = (record: GateDataRecordVO) => { |
||||
uni.navigateTo({ |
||||
url: `/pages/gate/detail?id=${record.id}` |
||||
}) |
||||
} |
||||
|
||||
// 预览图片 |
||||
const previewImages = (images: string[], index: number) => { |
||||
gateStore.previewImage(images[index], images) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
// 直接初始化数据,确保页面加载时就有数据 |
||||
gateStore.getHistoryData(selectedStation.value?.id) |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.page-container { |
||||
height: 100vh; |
||||
display: flex; |
||||
flex-direction: column; |
||||
background: #f5f5f5; |
||||
width: 100%; |
||||
overflow: hidden; |
||||
box-sizing: border-box; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
// 顶部标题 |
||||
.header { |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
padding: 60rpx 0 30rpx; |
||||
flex-shrink: 0; |
||||
|
||||
.title { |
||||
font-size: 48rpx; |
||||
font-weight: 600; |
||||
color: white; |
||||
padding: 0 20rpx; |
||||
} |
||||
} |
||||
|
||||
// 筛选栏 |
||||
.filter-bar { |
||||
background: white; |
||||
padding: 20rpx; |
||||
display: flex; |
||||
gap: 20rpx; |
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); |
||||
flex-shrink: 0; |
||||
margin: 0; |
||||
|
||||
.filter-item { |
||||
flex: 1; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
gap: 12rpx; |
||||
padding: 20rpx; |
||||
background: #f7f8fa; |
||||
border-radius: 12rpx; |
||||
transition: all 0.2s; |
||||
|
||||
&:active { |
||||
background: #e8eaed; |
||||
transform: scale(0.98); |
||||
} |
||||
|
||||
.filter-icon { |
||||
font-size: 32rpx; |
||||
color: #667eea; |
||||
} |
||||
|
||||
.filter-text { |
||||
font-size: 28rpx; |
||||
color: #333; |
||||
} |
||||
|
||||
.filter-arrow { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 统计容器 |
||||
.stats-container { |
||||
background: white; |
||||
margin: 20rpx 0; |
||||
border-radius: 16rpx; |
||||
padding: 30rpx 20rpx; |
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); |
||||
flex-shrink: 0; |
||||
|
||||
.stats-row { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-around; |
||||
} |
||||
|
||||
.stat-item { |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
flex: 1; |
||||
|
||||
.stat-value { |
||||
font-size: 44rpx; |
||||
font-weight: 600; |
||||
color: #667eea; |
||||
} |
||||
|
||||
.stat-label { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
margin-top: 8rpx; |
||||
} |
||||
} |
||||
|
||||
.stat-divider { |
||||
width: 2rpx; |
||||
height: 60rpx; |
||||
background: #f0f0f0; |
||||
} |
||||
} |
||||
|
||||
// 数据列表容器 |
||||
.data-list-container { |
||||
flex: 1; |
||||
padding: 0 0 40rpx; |
||||
overflow-y: auto; |
||||
-webkit-overflow-scrolling: touch; |
||||
} |
||||
|
||||
.data-list { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 20rpx; |
||||
padding: 0 20rpx; |
||||
} |
||||
|
||||
// 数据卡片 |
||||
.data-card { |
||||
background: white; |
||||
border-radius: 16rpx; |
||||
padding: 20rpx; |
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05); |
||||
transition: all 0.2s; |
||||
|
||||
&:active { |
||||
transform: scale(0.98); |
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.card-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: flex-start; |
||||
margin-bottom: 20rpx; |
||||
|
||||
.station-info { |
||||
flex: 1; |
||||
|
||||
.station-name { |
||||
font-size: 32rpx; |
||||
font-weight: 600; |
||||
color: #333; |
||||
display: block; |
||||
margin-bottom: 8rpx; |
||||
} |
||||
|
||||
.record-time { |
||||
font-size: 26rpx; |
||||
color: #999; |
||||
} |
||||
} |
||||
|
||||
.status-badge { |
||||
padding: 8rpx 16rpx; |
||||
border-radius: 20rpx; |
||||
font-size: 24rpx; |
||||
|
||||
&.status-approved { |
||||
background: #f6ffed; |
||||
color: #52c41a; |
||||
} |
||||
|
||||
&.status-pending { |
||||
background: #fff7e6; |
||||
color: #fa8c16; |
||||
} |
||||
|
||||
&.status-rejected { |
||||
background: #fff2f0; |
||||
color: #f5222d; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.card-content { |
||||
.data-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
} |
||||
|
||||
.data-item { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 12rpx; |
||||
|
||||
.data-icon { |
||||
font-size: 32rpx; |
||||
|
||||
&.blue { |
||||
color: #667eea; |
||||
} |
||||
|
||||
&.green { |
||||
color: #52c41a; |
||||
} |
||||
} |
||||
|
||||
.data-value { |
||||
font-size: 36rpx; |
||||
font-weight: 600; |
||||
color: #333; |
||||
} |
||||
|
||||
.data-unit { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
} |
||||
|
||||
.data-label { |
||||
font-size: 28rpx; |
||||
color: #52c41a; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.image-preview { |
||||
display: flex; |
||||
gap: 16rpx; |
||||
margin-top: 20rpx; |
||||
|
||||
.preview-item { |
||||
width: 120rpx; |
||||
height: 120rpx; |
||||
border-radius: 12rpx; |
||||
overflow: hidden; |
||||
|
||||
.preview-img { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
} |
||||
|
||||
.more-images { |
||||
width: 120rpx; |
||||
height: 120rpx; |
||||
background: #f7f8fa; |
||||
border-radius: 12rpx; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
|
||||
.more-text { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 空状态 |
||||
.empty-state { |
||||
flex: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: center; |
||||
|
||||
.empty-icon { |
||||
font-size: 120rpx; |
||||
color: #ddd; |
||||
margin-bottom: 20rpx; |
||||
} |
||||
|
||||
.empty-text { |
||||
font-size: 32rpx; |
||||
color: #999; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,368 @@
@@ -0,0 +1,368 @@
|
||||
<template> |
||||
<RootView> |
||||
<view class="page-container"> |
||||
<!-- 顶部标题 --> |
||||
<view class="header"> |
||||
<text class="page-title">选择闸口</text> |
||||
<text v-if="selectedStation" class="selected-info">{{ selectedStation.name }}</text> |
||||
</view> |
||||
|
||||
<!-- 搜索框 --> |
||||
<view class="search-container"> |
||||
<view class="search-box"> |
||||
<i class="i-tabler-search search-icon"></i> |
||||
<input |
||||
v-model="searchKeyword" |
||||
type="text" |
||||
placeholder="搜索闸口名称或位置" |
||||
class="search-input" |
||||
@input="onSearch" |
||||
/> |
||||
<i v-if="searchKeyword" class="i-tabler-x clear-icon" @click="clearSearch"></i> |
||||
</view> |
||||
<text class="result-count">{{ filteredStations.length }} 个闸口</text> |
||||
</view> |
||||
|
||||
<!-- 闸口列表 --> |
||||
<scroll-view class="station-list" scroll-y enhanced :show-scrollbar="false"> |
||||
<view |
||||
v-for="station in filteredStations" |
||||
:key="station.id" |
||||
class="station-item" |
||||
:class="{ 'selected': selectedStation?.id === station.id }" |
||||
@click="selectStation(station)" |
||||
> |
||||
<view class="station-indicator"> |
||||
<view class="indicator-dot" :class="{ 'active': station.status === 'active' }"></view> |
||||
</view> |
||||
<view class="station-content"> |
||||
<text class="station-name">{{ station.name }}</text> |
||||
<text class="station-location">{{ station.location }}</text> |
||||
</view> |
||||
<view class="station-right"> |
||||
<text class="station-status" :class="getStatusClass(station.status)"> |
||||
{{ gateStatusMap[station.status] }} |
||||
</text> |
||||
<i class="i-tabler-check check-icon" v-if="selectedStation?.id === station.id"></i> |
||||
</view> |
||||
</view> |
||||
</scroll-view> |
||||
|
||||
<!-- 空状态 --> |
||||
<view v-if="filteredStations.length === 0" class="empty-state"> |
||||
<i class="i-tabler-building-factory-2"></i> |
||||
<text class="empty-text">{{ searchKeyword ? '未找到匹配的闸口' : '暂无可用闸口' }}</text> |
||||
</view> |
||||
|
||||
<!-- 底部按钮 --> |
||||
<view v-if="selectedStation" class="footer"> |
||||
<button class="confirm-btn" @click="confirmSelection"> |
||||
确认选择 |
||||
</button> |
||||
</view> |
||||
</view> |
||||
</RootView> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref, computed, onMounted } from 'vue' |
||||
import RootView from '@/@layout/RootView.vue' |
||||
import { useGateStore } from '@/stores/gate.store' |
||||
import { gateStatusMap } from '@/common/gate-data' |
||||
import type { GateStation } from '../../../types/dto/gate.dto' |
||||
|
||||
const gateStore = useGateStore() |
||||
|
||||
// 响应式数据 |
||||
const searchKeyword = ref('') |
||||
const selectedStation = ref<GateStation | null>(null) |
||||
|
||||
// 计算属性 |
||||
const filteredStations = computed(() => { |
||||
if (!searchKeyword.value) { |
||||
return gateStore.activeStations |
||||
} |
||||
|
||||
const keyword = searchKeyword.value.toLowerCase() |
||||
return gateStore.activeStations.filter(station => |
||||
station.name.toLowerCase().includes(keyword) || |
||||
station.location.toLowerCase().includes(keyword) |
||||
) |
||||
}) |
||||
|
||||
// 获取状态样式 |
||||
const getStatusClass = (status: string) => { |
||||
switch (status) { |
||||
case 'active': |
||||
return 'status-active' |
||||
case 'inactive': |
||||
return 'status-inactive' |
||||
case 'maintenance': |
||||
return 'status-maintenance' |
||||
default: |
||||
return 'status-default' |
||||
} |
||||
} |
||||
|
||||
// 获取状态背景颜色 |
||||
const getStatusBgColor = (status: string) => { |
||||
switch (status) { |
||||
case 'active': |
||||
return 'status-bg-active' |
||||
case 'inactive': |
||||
return 'status-bg-inactive' |
||||
case 'maintenance': |
||||
return 'status-bg-maintenance' |
||||
default: |
||||
return 'status-bg-default' |
||||
} |
||||
} |
||||
|
||||
// 清空搜索 |
||||
const clearSearch = () => { |
||||
searchKeyword.value = '' |
||||
} |
||||
|
||||
// 搜索处理 |
||||
const onSearch = () => { |
||||
// 搜索逻辑已在计算属性中处理 |
||||
} |
||||
|
||||
// 选择闸口 |
||||
const selectStation = (station: GateStation) => { |
||||
selectedStation.value = station |
||||
} |
||||
|
||||
// 确认选择 |
||||
const confirmSelection = () => { |
||||
if (selectedStation.value) { |
||||
gateStore.setCurrentStation(selectedStation.value) |
||||
uni.navigateBack() |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
// 设置默认选择第一个闸口 |
||||
if (gateStore.activeStations.length > 0 && !selectedStation.value) { |
||||
selectedStation.value = gateStore.activeStations[0] |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.page-container { |
||||
height: 100vh; |
||||
display: flex; |
||||
flex-direction: column; |
||||
background: #ffffff; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
// 顶部标题 |
||||
.header { |
||||
padding: 60rpx 40rpx 24rpx; |
||||
background: #ffffff; |
||||
border-bottom: 1rpx solid #f2f2f7; |
||||
|
||||
.page-title { |
||||
font-size: 56rpx; |
||||
font-weight: 600; |
||||
color: #000000; |
||||
display: block; |
||||
margin-bottom: 8rpx; |
||||
} |
||||
|
||||
.selected-info { |
||||
font-size: 28rpx; |
||||
color: #007aff; |
||||
} |
||||
} |
||||
|
||||
// 搜索区域 |
||||
.search-container { |
||||
padding: 24rpx 40rpx; |
||||
background: #ffffff; |
||||
border-bottom: 1rpx solid #f2f2f7; |
||||
|
||||
.search-box { |
||||
display: flex; |
||||
align-items: center; |
||||
background: #f2f2f7; |
||||
border-radius: 16rpx; |
||||
padding: 0 24rpx; |
||||
height: 80rpx; |
||||
margin-bottom: 16rpx; |
||||
|
||||
.search-icon { |
||||
font-size: 32rpx; |
||||
color: #8e8e93; |
||||
margin-right: 16rpx; |
||||
} |
||||
|
||||
.search-input { |
||||
flex: 1; |
||||
height: 100%; |
||||
font-size: 30rpx; |
||||
color: #000000; |
||||
background: transparent; |
||||
|
||||
&::placeholder { |
||||
color: #8e8e93; |
||||
} |
||||
} |
||||
|
||||
.clear-icon { |
||||
font-size: 28rpx; |
||||
color: #8e8e93; |
||||
padding: 8rpx; |
||||
margin-left: 16rpx; |
||||
} |
||||
} |
||||
|
||||
.result-count { |
||||
font-size: 26rpx; |
||||
color: #8e8e93; |
||||
} |
||||
} |
||||
|
||||
// 闸口列表 |
||||
.station-list { |
||||
flex: 1; |
||||
overflow: hidden; |
||||
padding: 0 40rpx; |
||||
} |
||||
|
||||
.station-item { |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 28rpx 0; |
||||
border-bottom: 1rpx solid #f2f2f7; |
||||
transition: background-color 0.2s ease; |
||||
|
||||
&:active { |
||||
background-color: #f2f2f7; |
||||
} |
||||
|
||||
&:last-child { |
||||
border-bottom: none; |
||||
} |
||||
|
||||
&.selected { |
||||
.station-content .station-name { |
||||
color: #007aff; |
||||
} |
||||
} |
||||
|
||||
.station-indicator { |
||||
margin-right: 24rpx; |
||||
|
||||
.indicator-dot { |
||||
width: 12rpx; |
||||
height: 12rpx; |
||||
border-radius: 50%; |
||||
background: #c7c7cc; |
||||
|
||||
&.active { |
||||
background: #34c759; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.station-content { |
||||
flex: 1; |
||||
|
||||
.station-name { |
||||
font-size: 32rpx; |
||||
color: #000000; |
||||
display: block; |
||||
margin-bottom: 6rpx; |
||||
} |
||||
|
||||
.station-location { |
||||
font-size: 26rpx; |
||||
color: #8e8e93; |
||||
display: block; |
||||
} |
||||
} |
||||
|
||||
.station-right { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 16rpx; |
||||
|
||||
.station-status { |
||||
font-size: 24rpx; |
||||
padding: 6rpx 12rpx; |
||||
border-radius: 8rpx; |
||||
|
||||
&.status-active { |
||||
background: #f0f9ff; |
||||
color: #007aff; |
||||
} |
||||
|
||||
&.status-inactive { |
||||
background: #f2f2f7; |
||||
color: #8e8e93; |
||||
} |
||||
|
||||
&.status-maintenance { |
||||
background: #fff7ed; |
||||
color: #ff9500; |
||||
} |
||||
} |
||||
|
||||
.check-icon { |
||||
font-size: 32rpx; |
||||
color: #34c759; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 空状态 |
||||
.empty-state { |
||||
flex: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
justify-content: center; |
||||
padding: 120rpx 40rpx; |
||||
|
||||
i { |
||||
font-size: 120rpx; |
||||
color: #c7c7cc; |
||||
margin-bottom: 32rpx; |
||||
} |
||||
|
||||
.empty-text { |
||||
font-size: 30rpx; |
||||
color: #8e8e93; |
||||
} |
||||
} |
||||
|
||||
// 底部按钮 |
||||
.footer { |
||||
padding: 24rpx 40rpx 40rpx; |
||||
background: #ffffff; |
||||
border-top: 1rpx solid #f2f2f7; |
||||
|
||||
.confirm-btn { |
||||
width: 100%; |
||||
height: 112rpx; |
||||
background: #007aff; |
||||
border: none; |
||||
border-radius: 28rpx; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
color: #ffffff; |
||||
font-size: 34rpx; |
||||
font-weight: 600; |
||||
transition: all 0.2s ease; |
||||
|
||||
&:active { |
||||
opacity: 0.8; |
||||
transform: scale(0.98); |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,616 @@
@@ -0,0 +1,616 @@
|
||||
<template> |
||||
<RootView> |
||||
<view class="container"> |
||||
<!-- 顶部信息 --> |
||||
<view class="header"> |
||||
<view class="header-bg"></view> |
||||
<view class="header-content"> |
||||
<text class="title">数据上传</text> |
||||
<view v-if="currentStation" class="station-info" @click="changeStation"> |
||||
<i class="i-tabler-map-2"></i> |
||||
<text class="station-name">{{ currentStation.name }}</text> |
||||
<i class="i-tabler-chevron-right"></i> |
||||
</view> |
||||
<view v-else class="no-station" @click="selectStation"> |
||||
<i class="i-tabler-map-pin"></i> |
||||
<text>选择闸口站点</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 内容区域 --> |
||||
<scroll-view class="content" scroll-y enhanced :show-scrollbar="false"> |
||||
<!-- 图片上传 --> |
||||
<view class="section"> |
||||
<view class="section-title"> |
||||
<i class="i-tabler-camera" /> |
||||
<text>现场照片</text> |
||||
<text class="count">{{ uploadImages.length }}/3</text> |
||||
</view> |
||||
<view class="image-list"> |
||||
<view |
||||
v-for="(image, index) in uploadImages" |
||||
:key="index" |
||||
class="image-item" |
||||
@click="previewImage(image, uploadImages)" |
||||
> |
||||
<image :src="image" mode="aspectFill" /> |
||||
<view class="delete" @click.stop="removeImage(index)"> |
||||
<i class="i-tabler-x" /> |
||||
</view> |
||||
</view> |
||||
<view |
||||
v-if="uploadImages.length < 3" |
||||
class="add-image" |
||||
@click="chooseImages" |
||||
> |
||||
<i class="i-tabler-plus" /> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 测流数据 --> |
||||
<view class="section"> |
||||
<view class="section-title"> |
||||
<i class="i-tabler-droplet" /> |
||||
<text>测流数据</text> |
||||
</view> |
||||
<view class="input-group"> |
||||
<input |
||||
v-model="flowValue" |
||||
type="digit" |
||||
placeholder="请输入测流值" |
||||
class="input" |
||||
/> |
||||
<text class="unit">m³/s</text> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 定位信息 --> |
||||
<view class="section"> |
||||
<view class="section-title"> |
||||
<i class="i-tabler-location" /> |
||||
<text>定位信息</text> |
||||
</view> |
||||
<view class="location-inputs"> |
||||
<view class="coord-input"> |
||||
<text class="label">纬度</text> |
||||
<input |
||||
v-model="latitude" |
||||
type="text" |
||||
placeholder="点击获取定位" |
||||
class="coord-field" |
||||
disabled |
||||
/> |
||||
</view> |
||||
<view class="coord-input"> |
||||
<text class="label">经度</text> |
||||
<input |
||||
v-model="longitude" |
||||
type="text" |
||||
placeholder="点击获取定位" |
||||
class="coord-field" |
||||
disabled |
||||
/> |
||||
</view> |
||||
</view> |
||||
<view class="location-btn" @click="getLocation"> |
||||
<i class="i-tabler-map-pin" /> |
||||
<text>{{ locationLoading ? '定位中...' : '获取定位' }}</text> |
||||
</view> |
||||
</view> |
||||
</scroll-view> |
||||
|
||||
<!-- 底部按钮 --> |
||||
<view class="footer"> |
||||
<button |
||||
class="submit-btn" |
||||
:disabled="!canSubmit || submitting" |
||||
@click="submitData" |
||||
> |
||||
<text>{{ submitting ? '提交中...' : '提交数据' }}</text> |
||||
</button> |
||||
</view> |
||||
</view> |
||||
</RootView> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { ref, computed, onMounted } from 'vue' |
||||
import RootView from '@/@layout/RootView.vue' |
||||
import { useGateStore } from '@/stores/gate.store' |
||||
import type { GateDataUploadDTO } from '../../../types/dto/gate.dto' |
||||
|
||||
const gateStore = useGateStore() |
||||
|
||||
// 响应式数据 |
||||
const flowValue = ref('') |
||||
const remark = ref('') |
||||
const submitting = ref(false) |
||||
const locationLoading = ref(false) |
||||
const latitude = ref('') |
||||
const longitude = ref('') |
||||
|
||||
// 计算属性 |
||||
const currentStation = computed(() => gateStore.currentStation) |
||||
const uploadImages = computed(() => gateStore.uploadImages) |
||||
|
||||
const canSubmit = computed(() => { |
||||
return currentStation.value && |
||||
flowValue.value && |
||||
Number(flowValue.value) > 0 && |
||||
uploadImages.value.length > 0 && |
||||
latitude.value && |
||||
longitude.value |
||||
}) |
||||
|
||||
// 选择闸口 |
||||
const selectStation = () => { |
||||
uni.navigateTo({ |
||||
url: '/pages/gate/list' |
||||
}) |
||||
} |
||||
|
||||
// 切换闸口 |
||||
const changeStation = () => { |
||||
selectStation() |
||||
} |
||||
|
||||
// 选择图片 |
||||
const chooseImages = async () => { |
||||
const remainingCount = 3 - uploadImages.value.length |
||||
await gateStore.chooseImages(remainingCount) |
||||
} |
||||
|
||||
// 预览图片 |
||||
const previewImage = (current: string, urls: string[]) => { |
||||
gateStore.previewImage(current, urls) |
||||
} |
||||
|
||||
// 删除图片 |
||||
const removeImage = (index: number) => { |
||||
gateStore.removeImage(index) |
||||
} |
||||
|
||||
// 获取定位 |
||||
const getLocation = async () => { |
||||
locationLoading.value = true |
||||
try { |
||||
const location = await gateStore.getCurrentLocation() |
||||
if (location) { |
||||
// 将经纬度填入输入框 |
||||
latitude.value = location.latitude.toFixed(6) |
||||
longitude.value = location.longitude.toFixed(6) |
||||
uni.showToast({ |
||||
title: '定位获取成功', |
||||
icon: 'success' |
||||
}) |
||||
} |
||||
} catch (error) { |
||||
uni.showToast({ |
||||
title: '定位获取失败,请检查权限设置', |
||||
icon: 'error' |
||||
}) |
||||
} finally { |
||||
locationLoading.value = false |
||||
} |
||||
} |
||||
|
||||
// 提交数据 |
||||
const submitData = async () => { |
||||
if (!canSubmit.value) return |
||||
|
||||
submitting.value = true |
||||
|
||||
try { |
||||
const uploadData: GateDataUploadDTO = { |
||||
stationId: currentStation.value!.id, |
||||
flowValue: Number(flowValue.value), |
||||
images: uploadImages.value, |
||||
location: { |
||||
latitude: Number(latitude.value), |
||||
longitude: Number(longitude.value) |
||||
}, |
||||
remark: remark.value || undefined |
||||
} |
||||
|
||||
const success = await gateStore.uploadGateData(uploadData) |
||||
|
||||
if (success) { |
||||
uni.showToast({ |
||||
title: '数据上传成功', |
||||
icon: 'success' |
||||
}) |
||||
|
||||
// 重置表单 |
||||
flowValue.value = '' |
||||
remark.value = '' |
||||
latitude.value = '' |
||||
longitude.value = '' |
||||
gateStore.clearUploadImages() |
||||
|
||||
// 延迟返回首页 |
||||
setTimeout(() => { |
||||
uni.switchTab({ |
||||
url: '/pages/gate/history' |
||||
}) |
||||
}, 1500) |
||||
} else { |
||||
uni.showToast({ |
||||
title: gateStore.error || '上传失败', |
||||
icon: 'error' |
||||
}) |
||||
} |
||||
} catch (error) { |
||||
uni.showToast({ |
||||
title: '上传失败', |
||||
icon: 'error' |
||||
}) |
||||
} finally { |
||||
submitting.value = false |
||||
} |
||||
} |
||||
|
||||
onMounted(() => { |
||||
// 如果没有选择闸口,自动选择第一个 |
||||
if (!currentStation.value && gateStore.activeStations.length > 0) { |
||||
gateStore.setCurrentStation(gateStore.activeStations[0]) |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.container { |
||||
height: 100vh; |
||||
display: flex; |
||||
flex-direction: column; |
||||
background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%); |
||||
overflow: hidden; |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
// 顶部区域 |
||||
.header { |
||||
position: relative; |
||||
padding: 60rpx 40rpx 60rpx; |
||||
overflow: hidden; |
||||
flex-shrink: 0; |
||||
|
||||
.header-bg { |
||||
position: absolute; |
||||
top: -50%; |
||||
right: -50%; |
||||
width: 200%; |
||||
height: 200%; |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
border-radius: 50%; |
||||
opacity: 0.06; |
||||
} |
||||
|
||||
.header-content { |
||||
position: relative; |
||||
z-index: 1; |
||||
|
||||
.title { |
||||
font-size: 56rpx; |
||||
font-weight: 700; |
||||
color: #1a1a1a; |
||||
display: block; |
||||
margin-bottom: 16rpx; |
||||
letter-spacing: 2rpx; |
||||
} |
||||
|
||||
.station-info { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 16rpx; |
||||
padding: 20rpx 24rpx; |
||||
background: #f5f5f7; |
||||
border-radius: 16rpx; |
||||
transition: background-color 0.2s ease; |
||||
|
||||
&:active { |
||||
background: #e5e5ea; |
||||
} |
||||
|
||||
i:first-child { |
||||
font-size: 28rpx; |
||||
color: #667eea; |
||||
} |
||||
|
||||
.station-name { |
||||
flex: 1; |
||||
font-size: 30rpx; |
||||
color: #1a1a1a; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
i:last-child { |
||||
font-size: 24rpx; |
||||
color: #c7c7cc; |
||||
} |
||||
} |
||||
|
||||
.no-station { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
gap: 12rpx; |
||||
padding: 20rpx; |
||||
background: #f5f5f7; |
||||
border-radius: 16rpx; |
||||
transition: background-color 0.2s ease; |
||||
|
||||
&:active { |
||||
background: #e5e5ea; |
||||
} |
||||
|
||||
i { |
||||
font-size: 28rpx; |
||||
color: #667eea; |
||||
} |
||||
|
||||
text { |
||||
font-size: 30rpx; |
||||
color: #667eea; |
||||
font-weight: 500; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 内容区域 |
||||
.content { |
||||
flex: 1; |
||||
padding: 0; |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
.section { |
||||
margin-bottom: 40rpx; |
||||
width: 100%; |
||||
padding: 0 20rpx; |
||||
box-sizing: border-box; |
||||
|
||||
.section-title { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 12rpx; |
||||
margin-bottom: 24rpx; |
||||
|
||||
i { |
||||
font-size: 32rpx; |
||||
color: #667eea; |
||||
flex-shrink: 0; |
||||
} |
||||
|
||||
text { |
||||
font-size: 34rpx; |
||||
font-weight: 600; |
||||
color: #1a1a1a; |
||||
flex-shrink: 0; |
||||
} |
||||
|
||||
.count { |
||||
margin-left: auto; |
||||
font-size: 26rpx; |
||||
color: #999; |
||||
flex-shrink: 0; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 图片列表 |
||||
.image-list { |
||||
display: flex; |
||||
gap: 20rpx; |
||||
flex-wrap: wrap; |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
|
||||
.image-item { |
||||
width: calc(33.333% - 14rpx); |
||||
aspect-ratio: 1; |
||||
position: relative; |
||||
border-radius: 16rpx; |
||||
overflow: hidden; |
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08); |
||||
box-sizing: border-box; |
||||
|
||||
image { |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
.delete { |
||||
position: absolute; |
||||
top: 8rpx; |
||||
right: 8rpx; |
||||
width: 40rpx; |
||||
height: 40rpx; |
||||
background: rgba(0, 0, 0, 0.6); |
||||
border-radius: 50%; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
backdrop-filter: blur(10rpx); |
||||
|
||||
i { |
||||
font-size: 20rpx; |
||||
color: white; |
||||
} |
||||
} |
||||
} |
||||
|
||||
.add-image { |
||||
width: calc(33.333% - 14rpx); |
||||
aspect-ratio: 1; |
||||
border: 2rpx dashed #ddd; |
||||
border-radius: 16rpx; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
background: white; |
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04); |
||||
transition: all 0.2s ease; |
||||
box-sizing: border-box; |
||||
|
||||
&:active { |
||||
background: #f8f8f8; |
||||
border-color: #667eea; |
||||
} |
||||
|
||||
i { |
||||
font-size: 48rpx; |
||||
color: #ccc; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 输入框 |
||||
.input-group { |
||||
display: flex; |
||||
align-items: center; |
||||
background: white; |
||||
border-radius: 16rpx; |
||||
padding: 0 24rpx; |
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
|
||||
.input { |
||||
flex: 1; |
||||
height: 96rpx; |
||||
font-size: 32rpx; |
||||
color: #1a1a1a; |
||||
|
||||
&::placeholder { |
||||
color: #999; |
||||
} |
||||
} |
||||
|
||||
.unit { |
||||
font-size: 28rpx; |
||||
color: #999; |
||||
margin-left: 12rpx; |
||||
} |
||||
} |
||||
|
||||
// 定位输入区域 |
||||
.location-inputs { |
||||
display: flex; |
||||
gap: 20rpx; |
||||
margin-bottom: 20rpx; |
||||
|
||||
.coord-input { |
||||
flex: 1; |
||||
|
||||
.label { |
||||
font-size: 26rpx; |
||||
color: #666; |
||||
display: block; |
||||
margin-bottom: 12rpx; |
||||
} |
||||
|
||||
.coord-field { |
||||
width: 100%; |
||||
height: 88rpx; |
||||
background: white; |
||||
border: none; |
||||
border-radius: 16rpx; |
||||
padding: 0 20rpx; |
||||
font-size: 30rpx; |
||||
color: #1a1a1a; |
||||
box-sizing: border-box; |
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); |
||||
|
||||
&:disabled { |
||||
background: #f8f8f8; |
||||
color: #999; |
||||
} |
||||
|
||||
&::placeholder { |
||||
color: #ccc; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
.location-btn { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
gap: 12rpx; |
||||
width: 100%; |
||||
height: 88rpx; |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
border-radius: 16rpx; |
||||
transition: all 0.2s ease; |
||||
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3); |
||||
box-sizing: border-box; |
||||
|
||||
&:active { |
||||
transform: translateY(-2rpx); |
||||
box-shadow: 0 12rpx 32rpx rgba(102, 126, 234, 0.4); |
||||
} |
||||
|
||||
i { |
||||
font-size: 28rpx; |
||||
color: white; |
||||
} |
||||
|
||||
text { |
||||
font-size: 30rpx; |
||||
color: white; |
||||
font-weight: 500; |
||||
} |
||||
} |
||||
|
||||
// 底部按钮 |
||||
.footer { |
||||
padding: 30rpx 20rpx 40rpx; |
||||
background: white; |
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04); |
||||
flex-shrink: 0; |
||||
|
||||
.submit-btn { |
||||
width: 100%; |
||||
height: 100rpx; |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
border-radius: 24rpx; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
border: none; |
||||
transition: all 0.2s ease; |
||||
box-shadow: 0 8rpx 32rpx rgba(102, 126, 234, 0.3); |
||||
|
||||
&:not(:disabled) { |
||||
text { |
||||
color: white; |
||||
} |
||||
|
||||
&:active { |
||||
transform: translateY(-2rpx); |
||||
box-shadow: 0 12rpx 40rpx rgba(102, 126, 234, 0.4); |
||||
} |
||||
} |
||||
|
||||
&:disabled { |
||||
background: #f0f0f0; |
||||
box-shadow: none; |
||||
|
||||
text { |
||||
color: #999; |
||||
} |
||||
} |
||||
|
||||
text { |
||||
font-size: 34rpx; |
||||
font-weight: 600; |
||||
transition: color 0.2s ease; |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,490 @@
@@ -0,0 +1,490 @@
|
||||
<template> |
||||
<RootView> |
||||
<view class="page-container"> |
||||
<view class="container"> |
||||
<!-- 顶部信息区域 --> |
||||
<view class="header"> |
||||
<view class="header-bg"></view> |
||||
<view class="header-content"> |
||||
<text class="title">闸口数据管理</text> |
||||
<text class="subtitle">实时监控 · 智能管理 · 高效运维</text> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 数据概览 --> |
||||
<view class="stats-grid"> |
||||
<view class="stat-card"> |
||||
<view class="stat-icon"> |
||||
<i class="i-tabler-chart-dots-3"></i> |
||||
</view> |
||||
<view class="stat-info"> |
||||
<text class="stat-value">{{ activeStations.length }}</text> |
||||
<text class="stat-label">监测站点</text> |
||||
</view> |
||||
</view> |
||||
<view class="stat-card"> |
||||
<view class="stat-icon"> |
||||
<i class="i-tabler-database"></i> |
||||
</view> |
||||
<view class="stat-info"> |
||||
<text class="stat-value">{{ historyData.length }}</text> |
||||
<text class="stat-label">数据记录</text> |
||||
</view> |
||||
</view> |
||||
<view class="stat-card"> |
||||
<view class="stat-icon"> |
||||
<i class="i-tabler-wifi"></i> |
||||
</view> |
||||
<view class="stat-info"> |
||||
<text class="stat-value">98%</text> |
||||
<text class="stat-label">在线率</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 站点列表 --> |
||||
<view class="section"> |
||||
<view class="section-header"> |
||||
<text class="section-title">闸口站点</text> |
||||
<view class="section-right"> |
||||
<text class="station-count">{{ activeStations.length }}个站点</text> |
||||
<i class="i-tabler-chevron-right"></i> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 可滚动的站点列表容器 --> |
||||
<view class="station-scroll-container"> |
||||
<scroll-view |
||||
class="station-scroll" |
||||
scroll-y |
||||
enhanced |
||||
show-scrollbar="false" |
||||
> |
||||
<view class="station-grid"> |
||||
<view |
||||
v-for="station in activeStations" |
||||
:key="station.id" |
||||
class="station-card" |
||||
@click="selectStation(station)" |
||||
> |
||||
<view class="station-header"> |
||||
<view class="station-status" :class="{ 'status-active': station.status === 'active' }"></view> |
||||
<text class="station-name">{{ station.name }}</text> |
||||
</view> |
||||
<text class="station-location">{{ station.location }}</text> |
||||
</view> |
||||
</view> |
||||
</scroll-view> |
||||
</view> |
||||
</view> |
||||
|
||||
<!-- 快捷操作 --> |
||||
<view class="action-section"> |
||||
<view class="action-card primary" @click="goToUpload"> |
||||
<view class="action-icon"> |
||||
<i class="i-tabler-cloud-upload"></i> |
||||
</view> |
||||
<view class="action-content"> |
||||
<text class="action-title">数据上传</text> |
||||
<text class="action-desc">快速上传闸口数据</text> |
||||
</view> |
||||
<i class="i-tabler-arrow-right"></i> |
||||
</view> |
||||
<view class="action-card secondary" @click="goToHistory"> |
||||
<view class="action-icon"> |
||||
<i class="i-tabler-history"></i> |
||||
</view> |
||||
<view class="action-content"> |
||||
<text class="action-title">历史记录</text> |
||||
<text class="action-desc">查看历史数据</text> |
||||
</view> |
||||
<i class="i-tabler-arrow-right"></i> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</RootView> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import { onMounted, computed } from 'vue' |
||||
import PageTitle from '@/@layout/PageTitle.vue' |
||||
import RootView from '@/@layout/RootView.vue' |
||||
import { useGateStore } from '@/stores/gate.store' |
||||
import { gateStatusMap } from '@/common/gate-data' |
||||
import type { GateStation } from '../../../types/dto/gate.dto' |
||||
|
||||
const gateStore = useGateStore() |
||||
|
||||
const activeStations = computed(() => gateStore.activeStations) |
||||
const loading = computed(() => gateStore.loading) |
||||
const historyData = computed(() => gateStore.historyData) |
||||
|
||||
// 选择闸口站点 |
||||
const selectStation = (station: GateStation) => { |
||||
gateStore.setCurrentStation(station) |
||||
uni.navigateTo({ |
||||
url: '/pages/gate/upload' |
||||
}) |
||||
} |
||||
|
||||
// 前往上传页面 |
||||
const goToUpload = () => { |
||||
uni.navigateTo({ |
||||
url: '/pages/gate/upload' |
||||
}) |
||||
} |
||||
|
||||
// 前往历史记录页面 |
||||
const goToHistory = () => { |
||||
uni.navigateTo({ |
||||
url: '/pages/gate/history' |
||||
}) |
||||
} |
||||
|
||||
onMounted(() => { |
||||
// 页面加载时获取历史数据 |
||||
gateStore.getHistoryData() |
||||
}) |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.page-container { |
||||
height: 100vh; |
||||
background: linear-gradient(to bottom, #f8f9fa 0%, #ffffff 100%); |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.container { |
||||
height: 100vh; |
||||
padding: 0; |
||||
position: relative; |
||||
display: flex; |
||||
flex-direction: column; |
||||
} |
||||
|
||||
// 顶部区域 |
||||
.header { |
||||
position: relative; |
||||
padding: 60rpx 40rpx 60rpx; |
||||
overflow: hidden; |
||||
flex-shrink: 0; |
||||
|
||||
.header-bg { |
||||
position: absolute; |
||||
top: -50%; |
||||
right: -50%; |
||||
width: 200%; |
||||
height: 200%; |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
border-radius: 50%; |
||||
opacity: 0.06; |
||||
} |
||||
|
||||
.header-content { |
||||
position: relative; |
||||
z-index: 1; |
||||
|
||||
.title { |
||||
font-size: 56rpx; |
||||
font-weight: 700; |
||||
color: #1a1a1a; |
||||
display: block; |
||||
margin-bottom: 16rpx; |
||||
letter-spacing: 2rpx; |
||||
} |
||||
|
||||
.subtitle { |
||||
font-size: 28rpx; |
||||
color: #666; |
||||
display: block; |
||||
letter-spacing: 1rpx; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 数据概览 |
||||
.stats-grid { |
||||
padding: 0 40rpx; |
||||
margin-bottom: 40rpx; |
||||
display: grid; |
||||
grid-template-columns: repeat(3, 1fr); |
||||
gap: 24rpx; |
||||
flex-shrink: 0; |
||||
|
||||
.stat-card { |
||||
background: white; |
||||
border-radius: 32rpx; |
||||
padding: 32rpx 24rpx; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.04); |
||||
transition: all 0.3s ease; |
||||
|
||||
&:active { |
||||
transform: translateY(-4rpx); |
||||
box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.08); |
||||
} |
||||
|
||||
.stat-icon { |
||||
width: 80rpx; |
||||
height: 80rpx; |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
border-radius: 24rpx; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
margin-bottom: 20rpx; |
||||
|
||||
i { |
||||
font-size: 40rpx; |
||||
color: white; |
||||
} |
||||
} |
||||
|
||||
.stat-info { |
||||
text-align: left; |
||||
width: 100%; |
||||
|
||||
.stat-value { |
||||
font-size: 48rpx; |
||||
font-weight: 700; |
||||
color: #1a1a1a; |
||||
display: block; |
||||
line-height: 1.2; |
||||
margin-left: -8rpx; |
||||
} |
||||
|
||||
.stat-label { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
display: block; |
||||
margin-top: 8rpx; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 站点列表 |
||||
.section { |
||||
padding: 0 40rpx; |
||||
flex: 1; |
||||
display: flex; |
||||
flex-direction: column; |
||||
min-height: 0; |
||||
|
||||
.section-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 30rpx; |
||||
flex-shrink: 0; |
||||
|
||||
.section-title { |
||||
font-size: 36rpx; |
||||
font-weight: 600; |
||||
color: #1a1a1a; |
||||
letter-spacing: 1rpx; |
||||
} |
||||
|
||||
.section-right { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 12rpx; |
||||
|
||||
.station-count { |
||||
font-size: 28rpx; |
||||
color: #666; |
||||
} |
||||
|
||||
i { |
||||
font-size: 24rpx; |
||||
color: #999; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 站点滚动容器 |
||||
.station-scroll-container { |
||||
flex: 1; |
||||
overflow: hidden; |
||||
border-radius: 24rpx; |
||||
background: white; |
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.03); |
||||
|
||||
.station-scroll { |
||||
height: 100%; |
||||
} |
||||
} |
||||
|
||||
.station-grid { |
||||
display: grid; |
||||
grid-template-columns: repeat(2, 1fr); |
||||
gap: 2rpx; |
||||
padding: 2rpx; |
||||
|
||||
.station-card { |
||||
background: white; |
||||
padding: 32rpx; |
||||
transition: all 0.3s ease; |
||||
|
||||
&:active { |
||||
background: #f8f8f8; |
||||
} |
||||
|
||||
.station-header { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 16rpx; |
||||
margin-bottom: 16rpx; |
||||
|
||||
.station-status { |
||||
width: 16rpx; |
||||
height: 16rpx; |
||||
border-radius: 50%; |
||||
background: #e0e0e0; |
||||
transition: background 0.3s ease; |
||||
|
||||
&.status-active { |
||||
background: #52c41a; |
||||
box-shadow: 0 0 0 8rpx rgba(82, 196, 26, 0.1); |
||||
} |
||||
} |
||||
|
||||
.station-name { |
||||
font-size: 32rpx; |
||||
font-weight: 600; |
||||
color: #1a1a1a; |
||||
} |
||||
} |
||||
|
||||
.station-location { |
||||
font-size: 26rpx; |
||||
color: #999; |
||||
display: block; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 快捷操作 |
||||
.action-section { |
||||
padding: 40rpx; |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 24rpx; |
||||
flex-shrink: 0; |
||||
|
||||
.action-card { |
||||
background: white; |
||||
border-radius: 32rpx; |
||||
padding: 40rpx 32rpx; |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 24rpx; |
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.04); |
||||
transition: all 0.3s ease; |
||||
position: relative; |
||||
overflow: hidden; |
||||
|
||||
&:active { |
||||
transform: translateY(-4rpx); |
||||
box-shadow: 0 12rpx 48rpx rgba(0, 0, 0, 0.08); |
||||
} |
||||
|
||||
&.primary { |
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
||||
color: white; |
||||
|
||||
.action-icon { |
||||
background: rgba(255, 255, 255, 0.2); |
||||
|
||||
i { |
||||
color: white; |
||||
} |
||||
} |
||||
|
||||
.action-content { |
||||
.action-title { |
||||
color: white; |
||||
} |
||||
|
||||
.action-desc { |
||||
color: rgba(255, 255, 255, 0.8); |
||||
} |
||||
} |
||||
|
||||
i:last-child { |
||||
color: rgba(255, 255, 255, 0.8); |
||||
} |
||||
} |
||||
|
||||
&.secondary { |
||||
background: white; |
||||
|
||||
.action-icon { |
||||
background: #f5f5f7; |
||||
|
||||
i { |
||||
color: #667eea; |
||||
} |
||||
} |
||||
|
||||
.action-content { |
||||
.action-title { |
||||
color: #1a1a1a; |
||||
} |
||||
|
||||
.action-desc { |
||||
color: #999; |
||||
} |
||||
} |
||||
|
||||
i:last-child { |
||||
color: #ccc; |
||||
} |
||||
} |
||||
|
||||
.action-icon { |
||||
width: 96rpx; |
||||
height: 96rpx; |
||||
border-radius: 28rpx; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
flex-shrink: 0; |
||||
|
||||
i { |
||||
font-size: 48rpx; |
||||
transition: transform 0.3s ease; |
||||
} |
||||
} |
||||
|
||||
.action-content { |
||||
flex: 1; |
||||
|
||||
.action-title { |
||||
font-size: 32rpx; |
||||
font-weight: 600; |
||||
display: block; |
||||
margin-bottom: 8rpx; |
||||
} |
||||
|
||||
.action-desc { |
||||
font-size: 26rpx; |
||||
display: block; |
||||
} |
||||
} |
||||
|
||||
i:last-child { |
||||
font-size: 32rpx; |
||||
transition: transform 0.3s ease; |
||||
} |
||||
|
||||
&:active i:last-child { |
||||
transform: translateX(8rpx); |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
<template> |
||||
<RootView> |
||||
<PageTitle title="测试页面" /> |
||||
<view class="test-container"> |
||||
<view class="test-card"> |
||||
<text class="test-title">样式测试</text> |
||||
<view class="test-item"> |
||||
<text class="test-text">背景颜色测试</text> |
||||
</view> |
||||
<view class="test-item test-bg-blue"> |
||||
<text class="test-text">蓝色背景</text> |
||||
</view> |
||||
<view class="test-item test-flex"> |
||||
<text class="test-text">Flex布局测试</text> |
||||
</view> |
||||
</view> |
||||
</view> |
||||
</RootView> |
||||
</template> |
||||
|
||||
<script setup lang="ts"> |
||||
import PageTitle from '@/@layout/PageTitle.vue' |
||||
import RootView from '@/@layout/RootView.vue' |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.test-container { |
||||
padding: 40rpx; |
||||
background-color: #f5f5f5; |
||||
min-height: 100vh; |
||||
} |
||||
|
||||
.test-card { |
||||
background-color: #ffffff; |
||||
padding: 40rpx; |
||||
border-radius: 20rpx; |
||||
margin-bottom: 40rpx; |
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.test-title { |
||||
font-size: 40rpx; |
||||
font-weight: bold; |
||||
color: #333333; |
||||
margin-bottom: 40rpx; |
||||
display: block; |
||||
} |
||||
|
||||
.test-item { |
||||
padding: 30rpx; |
||||
margin-bottom: 20rpx; |
||||
border-radius: 12rpx; |
||||
background-color: #f8f9fa; |
||||
} |
||||
|
||||
.test-bg-blue { |
||||
background-color: #667eea !important; |
||||
} |
||||
|
||||
.test-flex { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
|
||||
.test-text { |
||||
font-size: 28rpx; |
||||
color: #333333; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
export {} |
||||
|
||||
declare module "vue" { |
||||
type Hooks = App.AppInstance & Page.PageInstance; |
||||
interface ComponentCustomOptions extends Hooks {} |
||||
} |
||||
|
After Width: | Height: | Size: 3.9 KiB |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
.light-layout { |
||||
--pri: 118 143 216; |
||||
--prid: 0 63 255; |
||||
--bg: 246 246 246; |
||||
--bg-sub: 221 235 241; |
||||
--bord: 231 231 231; |
||||
--text: 31 31 31; |
||||
--text2: 119 119 119; |
||||
--str: 255 255 255; |
||||
|
||||
} |
||||
|
||||
.dark-layout { |
||||
--pri: 118 143 216; |
||||
--prid: 0 63 255; |
||||
--bg: 16 16 16; |
||||
--bg-sub: 34 37 45; |
||||
--bord: 43 43 43; |
||||
--text: 255 255 255; |
||||
--text2: 119 119 119; |
||||
--str: 0 0 0; |
||||
} |
||||
|
||||
view, |
||||
text { |
||||
font-family: sans-serif; |
||||
} |
||||
@ -0,0 +1,266 @@
@@ -0,0 +1,266 @@
|
||||
import { defineStore } from 'pinia' |
||||
import type { GateStation, GateDataUploadDTO, Coordinates } from '../../types/dto/gate.dto' |
||||
import type { GateDataRecordVO, GateDataDetailVO } from '../../types/vo/gate.vo' |
||||
import { gateStations } from '@/common/gate-data' |
||||
|
||||
interface GateState { |
||||
stations: GateStation[] |
||||
currentStation: GateStation | null |
||||
currentLocation: Coordinates | null |
||||
uploadImages: string[] |
||||
historyData: GateDataRecordVO[] |
||||
loading: boolean |
||||
error: string | null |
||||
} |
||||
|
||||
export const useGateStore = defineStore('gate', { |
||||
state: (): GateState => ({ |
||||
stations: gateStations, |
||||
currentStation: null, |
||||
currentLocation: null, |
||||
uploadImages: [], |
||||
historyData: [], |
||||
loading: false, |
||||
error: null |
||||
}), |
||||
|
||||
getters: { |
||||
getStationById: (state) => (id: string) => { |
||||
return state.stations.find(station => station.id === id) |
||||
}, |
||||
|
||||
activeStations: (state) => { |
||||
return state.stations.filter(station => station.status === 'active') |
||||
} |
||||
}, |
||||
|
||||
actions: { |
||||
// 设置当前选中的闸口
|
||||
setCurrentStation(station: GateStation) { |
||||
this.currentStation = station |
||||
}, |
||||
|
||||
// 获取当前位置
|
||||
async getCurrentLocation(): Promise<Coordinates | null> { |
||||
this.loading = true |
||||
this.error = null |
||||
|
||||
try { |
||||
// 检查位置权限
|
||||
const authSetting = await uni.getSetting() |
||||
if (!authSetting.authSetting['scope.userLocation']) { |
||||
// 请求位置权限
|
||||
const authRes = await uni.authorize({ |
||||
scope: 'scope.userLocation' |
||||
}) |
||||
if (!authRes[1]) { |
||||
throw new Error('位置权限被拒绝') |
||||
} |
||||
} |
||||
|
||||
// 获取位置信息
|
||||
const locationRes = await uni.getLocation({ |
||||
type: 'gcj02', |
||||
isHighAccuracy: true, |
||||
altitude: true |
||||
}) |
||||
|
||||
const location: Coordinates = { |
||||
latitude: locationRes[1].latitude, |
||||
longitude: locationRes[1].longitude |
||||
} |
||||
|
||||
this.currentLocation = location |
||||
return location |
||||
} catch (error: any) { |
||||
this.error = error.message || '获取位置失败' |
||||
console.error('获取位置失败:', error) |
||||
return null |
||||
} finally { |
||||
this.loading = false |
||||
} |
||||
}, |
||||
|
||||
// 选择图片
|
||||
async chooseImages(count: number = 3): Promise<string[]> { |
||||
try { |
||||
const res = await uni.chooseImage({ |
||||
count, |
||||
sizeType: ['compressed', 'original'], |
||||
sourceType: ['album', 'camera'] |
||||
}) |
||||
|
||||
const tempFilePaths = res[1].tempFilePaths |
||||
this.uploadImages = [...this.uploadImages, ...tempFilePaths] |
||||
return tempFilePaths |
||||
} catch (error: any) { |
||||
this.error = error.message || '选择图片失败' |
||||
return [] |
||||
} |
||||
}, |
||||
|
||||
// 预览图片
|
||||
previewImage(current: string, urls: string[]) { |
||||
uni.previewImage({ |
||||
current, |
||||
urls |
||||
}) |
||||
}, |
||||
|
||||
// 删除图片
|
||||
removeImage(index: number) { |
||||
this.uploadImages.splice(index, 1) |
||||
}, |
||||
|
||||
// 清空上传图片
|
||||
clearUploadImages() { |
||||
this.uploadImages = [] |
||||
}, |
||||
|
||||
// 上传闸口数据
|
||||
async uploadGateData(data: GateDataUploadDTO): Promise<boolean> { |
||||
this.loading = true |
||||
this.error = null |
||||
|
||||
try { |
||||
// 这里应该调用API上传数据
|
||||
// 暂时模拟上传成功
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) |
||||
|
||||
// 生成模拟记录
|
||||
const record: GateDataRecordVO = { |
||||
id: Date.now().toString(), |
||||
stationId: data.stationId, |
||||
stationName: this.currentStation?.name || '', |
||||
flowValue: data.flowValue, |
||||
images: data.images, |
||||
location: data.location, |
||||
remark: data.remark, |
||||
createTime: new Date().toISOString(), |
||||
status: 'pending' |
||||
} |
||||
|
||||
this.historyData.unshift(record) |
||||
return true |
||||
} catch (error: any) { |
||||
this.error = error.message || '上传失败' |
||||
return false |
||||
} finally { |
||||
this.loading = false |
||||
} |
||||
}, |
||||
|
||||
// 获取历史数据
|
||||
async getHistoryData(stationId?: string): Promise<void> { |
||||
this.loading = true |
||||
this.error = null |
||||
|
||||
try { |
||||
// 这里应该调用API获取历史数据
|
||||
// 暂时使用模拟数据
|
||||
const mockData: GateDataRecordVO[] = [ |
||||
{ |
||||
id: '1', |
||||
stationId: 'GZ001', |
||||
stationName: '广州天河闸口', |
||||
flowValue: 125.5, |
||||
images: ['https://picsum.photos/400/300?random=1', 'https://picsum.photos/400/300?random=2'], |
||||
location: { latitude: 23.1291, longitude: 113.2644 }, |
||||
createTime: '2025-12-11T10:30:00', |
||||
status: 'approved' |
||||
}, |
||||
{ |
||||
id: '2', |
||||
stationId: 'SZ002', |
||||
stationName: '深圳南山闸口', |
||||
flowValue: 89.3, |
||||
images: ['https://picsum.photos/400/300?random=3'], |
||||
location: { latitude: 22.5431, longitude: 113.9491 }, |
||||
createTime: '2025-12-11T09:15:00', |
||||
status: 'pending' |
||||
}, |
||||
{ |
||||
id: '3', |
||||
stationId: 'GZ003', |
||||
stationName: '广州越秀闸口', |
||||
flowValue: 156.8, |
||||
images: ['https://picsum.photos/400/300?random=4', 'https://picsum.photos/400/300?random=5', 'https://picsum.photos/400/300?random=6'], |
||||
location: { latitude: 23.1256, longitude: 113.2698 }, |
||||
createTime: '2025-12-11T08:45:00', |
||||
status: 'approved' |
||||
}, |
||||
{ |
||||
id: '4', |
||||
stationId: 'SZ004', |
||||
stationName: '深圳福田闸口', |
||||
flowValue: 98.2, |
||||
images: ['https://picsum.photos/400/300?random=7', 'https://picsum.photos/400/300?random=8'], |
||||
location: { latitude: 22.5329, longitude: 114.0579 }, |
||||
createTime: '2025-12-10T16:20:00', |
||||
status: 'rejected' |
||||
}, |
||||
{ |
||||
id: '5', |
||||
stationId: 'GZ005', |
||||
stationName: '广州荔湾闸口', |
||||
flowValue: 201.4, |
||||
images: ['https://picsum.photos/400/300?random=9'], |
||||
location: { latitude: 23.1248, longitude: 113.2452 }, |
||||
createTime: '2025-12-10T14:10:00', |
||||
status: 'approved' |
||||
}, |
||||
{ |
||||
id: '6', |
||||
stationId: 'SZ006', |
||||
stationName: '深圳罗湖闸口', |
||||
flowValue: 143.7, |
||||
images: ['https://picsum.photos/400/300?random=10', 'https://picsum.photos/400/300?random=11', 'https://picsum.photos/400/300?random=12'], |
||||
location: { latitude: 22.5434, longitude: 114.1311 }, |
||||
createTime: '2025-12-10T11:30:00', |
||||
status: 'pending' |
||||
}, |
||||
{ |
||||
id: '7', |
||||
stationId: 'GZ007', |
||||
stationName: '广州海珠闸口', |
||||
flowValue: 87.9, |
||||
images: ['https://picsum.photos/400/300?random=13', 'https://picsum.photos/400/300?random=14'], |
||||
location: { latitude: 23.0833, longitude: 113.3245 }, |
||||
createTime: '2025-12-09T15:45:00', |
||||
status: 'approved' |
||||
}, |
||||
{ |
||||
id: '8', |
||||
stationId: 'SZ008', |
||||
stationName: '深圳宝安闸口', |
||||
flowValue: 167.3, |
||||
images: [], |
||||
location: { latitude: 22.5908, longitude: 113.8989 }, |
||||
createTime: '2025-12-09T13:20:00', |
||||
status: 'approved' |
||||
} |
||||
] |
||||
|
||||
// 根据站点ID筛选数据
|
||||
const filteredData = stationId |
||||
? mockData.filter(item => item.stationId === stationId) |
||||
: mockData |
||||
|
||||
// 更新历史数据
|
||||
this.historyData = filteredData |
||||
|
||||
console.log('历史数据已更新:', filteredData.length, '条记录') |
||||
} catch (error: any) { |
||||
this.error = error.message || '获取历史数据失败' |
||||
console.error('获取历史数据失败:', error) |
||||
} finally { |
||||
this.loading = false |
||||
} |
||||
}, |
||||
|
||||
// 清除错误
|
||||
clearError() { |
||||
this.error = null |
||||
} |
||||
} |
||||
}) |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
import { createPinia } from 'pinia' |
||||
|
||||
const pinia = createPinia() |
||||
|
||||
export default pinia |
||||
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
import { defineStore } from 'pinia' |
||||
import { computed } from 'vue' |
||||
import { ref } from 'vue' |
||||
export type ThemeType = 'light' | 'dark' |
||||
export const useThemeStore = defineStore('theme', () => { |
||||
const theme = ref<ThemeType>('light') |
||||
|
||||
const themeClass = computed(() => { |
||||
if (theme.value == 'light') { |
||||
return 'light-layout' |
||||
} |
||||
if (theme.value == 'dark') { |
||||
return 'dark-layout' |
||||
} |
||||
}) |
||||
|
||||
return { |
||||
theme, |
||||
themeClass |
||||
} |
||||
}) |
||||
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
/** |
||||
* 这里是uni-app内置的常用样式变量 |
||||
* |
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 |
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App |
||||
* |
||||
*/ |
||||
|
||||
/** |
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 |
||||
* |
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 |
||||
*/ |
||||
|
||||
/* 颜色变量 */ |
||||
@import 'uview-plus/theme.scss'; |
||||
/* 行为相关颜色 */ |
||||
$uni-color-primary: #007aff; |
||||
$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: #fff; |
||||
$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: 16; |
||||
|
||||
/* 图片尺寸 */ |
||||
$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: #555; // 二级标题颜色 |
||||
$uni-font-size-subtitle: 18px; |
||||
$uni-color-paragraph: #3f536e; // 文章段落颜色 |
||||
$uni-font-size-paragraph: 15px; |
||||
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
import { useToast } from '@/hooks/toast.use' |
||||
|
||||
interface RequestOpt { |
||||
url: string |
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' |
||||
data?: any |
||||
params?: any |
||||
headers?: Record<string, string> |
||||
timeout?: number |
||||
withoutToken?: boolean |
||||
donotGotoLogin?: boolean |
||||
withoutReject?: boolean |
||||
} |
||||
|
||||
interface RequestResponse<T = any> { |
||||
data: T |
||||
statusCode: number |
||||
header: Record<string, string> |
||||
cookies: string[] |
||||
} |
||||
|
||||
const baseURL = import.meta.env.VITE_BASE_URL |
||||
const timeout = 1000 * 6 |
||||
|
||||
const request = (options: RequestOpt) => { |
||||
const { |
||||
withoutToken, |
||||
url, |
||||
method = 'GET', |
||||
data, |
||||
donotGotoLogin, |
||||
params, |
||||
headers = {}, |
||||
withoutReject, |
||||
...rest |
||||
} = options |
||||
|
||||
// Handle authentication token
|
||||
let requestHeaders = { ...headers } |
||||
if (!withoutToken) { |
||||
const token = uni.getStorageSync('token') |
||||
if (token) { |
||||
requestHeaders = { |
||||
...requestHeaders, |
||||
Authorization: `Bearer ${token}` |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Format URL with query parameters if needed
|
||||
let requestUrl = url |
||||
if (params) { |
||||
const queryString = Object.keys(params) |
||||
.map( |
||||
key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}` |
||||
) |
||||
.join('&') |
||||
requestUrl += requestUrl.includes('?') |
||||
? '&' + queryString |
||||
: '?' + queryString |
||||
} |
||||
|
||||
return new Promise<any>((resolve, reject) => { |
||||
console.log('requestUrl: ', requestUrl) |
||||
|
||||
uni.request({ |
||||
url: requestUrl, |
||||
method: method, |
||||
data: data, |
||||
header: requestHeaders, |
||||
timeout: timeout, |
||||
withCredentials: true, |
||||
success: (response: RequestResponse) => { |
||||
const resp = response.data |
||||
console.log('【网络请求结果】: ', resp) |
||||
if (resp.code == 401) { |
||||
if (donotGotoLogin) { |
||||
reject(resp) |
||||
return |
||||
} |
||||
uni.hideLoading() |
||||
useToast(resp.msg) |
||||
return |
||||
} |
||||
if (!withoutReject) { |
||||
if (!resp.success) { |
||||
reject(resp) |
||||
useToast(resp.msg) |
||||
return |
||||
} |
||||
} |
||||
resolve(resp) |
||||
} |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
const createRequestIns = ({ |
||||
prefix = '', |
||||
ins |
||||
}: { |
||||
prefix?: string |
||||
ins?: any |
||||
}) => { |
||||
let instance = ins || request |
||||
const requestIns = (options: RequestOpt) => { |
||||
return instance({ |
||||
...options, |
||||
url: prefix + options.url |
||||
}) |
||||
} |
||||
return requestIns |
||||
} |
||||
|
||||
const baseRequestIns /* 通用 */ = createRequestIns({ |
||||
prefix: `${baseURL}` |
||||
}) |
||||
|
||||
export { request, createRequestIns, baseRequestIns } |
||||
export default { request } |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
{ |
||||
"extends": "@vue/tsconfig/tsconfig.json", |
||||
"compilerOptions": { |
||||
"sourceMap": true, |
||||
"baseUrl": ".", |
||||
"paths": { |
||||
"@/*": [ |
||||
"./src/*" |
||||
] |
||||
}, |
||||
"lib": [ |
||||
"esnext", |
||||
"dom" |
||||
], |
||||
"types": [ |
||||
"@dcloudio/types", |
||||
"@types/wechat-miniprogram", |
||||
"@uni-helper/uni-app-types", |
||||
"uview-plus/types" |
||||
] |
||||
}, |
||||
"vueCompilerOptions": { |
||||
"nativeTags": [ |
||||
"block", |
||||
"component", |
||||
"template", |
||||
"slot" |
||||
] |
||||
}, |
||||
"include": [ |
||||
"src/**/*.ts", |
||||
"src/**/*.d.ts", |
||||
"src/**/*.tsx", |
||||
"src/**/*.vue", |
||||
"types/**/*.d.ts", |
||||
"types/**/*.ts" |
||||
] |
||||
} |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
// 闸口站点相关类型定义
|
||||
|
||||
export interface Coordinates { |
||||
latitude: number |
||||
longitude: number |
||||
} |
||||
|
||||
export interface GateStation { |
||||
id: string |
||||
name: string |
||||
location: string |
||||
coordinates: Coordinates |
||||
description: string |
||||
status: 'active' | 'inactive' | 'maintenance' |
||||
} |
||||
|
||||
// 闸口数据上传请求DTO
|
||||
export interface GateDataUploadDTO { |
||||
stationId: string |
||||
flowValue: number // 测流值 m³/s
|
||||
images: string[] // 图片URL列表
|
||||
location: Coordinates // 实际测量位置
|
||||
remark?: string // 备注信息
|
||||
} |
||||
|
||||
// 闸口数据查询DTO
|
||||
export interface GateDataQueryDTO { |
||||
stationId?: string |
||||
startDate?: string |
||||
endDate?: string |
||||
page?: number |
||||
pageSize?: number |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
declare namespace SignDto { |
||||
/** |
||||
* 登录 |
||||
*/ |
||||
interface LoginDto { |
||||
uname: string; |
||||
pwd: string; |
||||
} |
||||
} |
||||
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
declare namespace Http { |
||||
interface IBase<T> { |
||||
code: number |
||||
success: boolean |
||||
data: T |
||||
msg: string |
||||
} |
||||
|
||||
type PageParams = { |
||||
pageNo: number |
||||
pageSize: number |
||||
} |
||||
|
||||
type IPage<T> = IBase<{ |
||||
totalPages: number |
||||
pageSize: number |
||||
pageNo: number |
||||
total: number |
||||
list: T[] |
||||
}> |
||||
|
||||
type IRes<T> = Promise<IBase<T>> |
||||
|
||||
type IPageRes<T> = Promise<IPage<T>> |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
// 闸口数据相关VO类型定义
|
||||
|
||||
import { Coordinates, GateStation } from '../dto/gate.dto' |
||||
|
||||
// 闸口数据记录VO
|
||||
export interface GateDataRecordVO { |
||||
id: string |
||||
stationId: string |
||||
stationName: string |
||||
flowValue: number |
||||
images: string[] |
||||
location: Coordinates |
||||
remark?: string |
||||
createTime: string |
||||
status: 'pending' | 'approved' | 'rejected' |
||||
} |
||||
|
||||
// 闸口数据详情VO
|
||||
export interface GateDataDetailVO extends GateDataRecordVO { |
||||
station: GateStation |
||||
uploader: { |
||||
id: string |
||||
name: string |
||||
avatar?: string |
||||
} |
||||
} |
||||
|
||||
// 闸口数据统计VO
|
||||
export interface GateDataStatsVO { |
||||
totalRecords: number |
||||
todayRecords: number |
||||
avgFlowValue: number |
||||
maxFlowValue: number |
||||
minFlowValue: number |
||||
} |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
declare namespace SignVo { |
||||
interface LoginVo { |
||||
accessToken: string |
||||
} |
||||
} |
||||
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
import { |
||||
defineConfig, |
||||
presetAttributify, |
||||
presetIcons, |
||||
transformerDirectives, |
||||
transformerVariantGroup |
||||
} from 'unocss' |
||||
|
||||
import presetWeapp from 'unocss-preset-weapp' |
||||
import { |
||||
extractorAttributify, |
||||
transformerClass |
||||
} from 'unocss-preset-weapp/transformer' |
||||
|
||||
const { presetWeappAttributify, transformerAttributify } = |
||||
extractorAttributify() |
||||
|
||||
/** |
||||
* unocss配置 |
||||
*/ |
||||
export default defineConfig({ |
||||
safelist: [], |
||||
theme: { |
||||
colors: { |
||||
pri: 'rgb(var(--pri))', |
||||
prid: 'rgb(var(--prid))', |
||||
bg: 'rgb(var(--bg))', |
||||
'bg-sub': 'rgb(var(--bg-sub))', |
||||
bord: 'rgb(var(--bord))', |
||||
text: 'rgb(var(--text))', |
||||
text2: 'rgb(var(--text2))', |
||||
str: 'rgb(var(--str))', |
||||
jd: 'rgb(var(--jd-color))' |
||||
} |
||||
}, |
||||
presets: [ |
||||
presetWeapp({ |
||||
platform: 'uniapp', |
||||
whRpx: false, |
||||
enableAttributify: true |
||||
}), |
||||
presetWeappAttributify(), |
||||
presetAttributify({ |
||||
prefix: 'un-', |
||||
prefixedOnly: false |
||||
}), |
||||
presetIcons({ |
||||
collections: { |
||||
tabler: () => import('@iconify-json/tabler').then(i => i.icons) |
||||
}, |
||||
scale: 1.2 |
||||
}) |
||||
], |
||||
content: { |
||||
pipeline: { |
||||
include: [ |
||||
// the default
|
||||
/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/, |
||||
// include js/ts files
|
||||
'src/**/*.{js,ts}' |
||||
] |
||||
} |
||||
}, |
||||
transformers: [ |
||||
transformerVariantGroup(), |
||||
transformerDirectives(), |
||||
transformerAttributify(), |
||||
transformerClass() |
||||
], |
||||
shortcuts: [ |
||||
{ |
||||
'flex-center': 'flex items-center justify-center', |
||||
divider: 'h-1px w-full bg-border my-2', |
||||
tap: 'active:(scale-60 opacity-50 rotate-180)', |
||||
'base-label': |
||||
'bg-bord text-text2 text-10px px-1 flex-center rd-3px inline-block h-18px font-500' |
||||
} |
||||
] |
||||
}) |
||||
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite' |
||||
import uni from '@dcloudio/vite-plugin-uni' |
||||
import { resolve } from 'path' |
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => { |
||||
const UnoCss = await import('unocss/vite').then(i => i.default) |
||||
|
||||
return { |
||||
plugins: [ |
||||
uni(), |
||||
// https://github.com/unocss/unocss
|
||||
UnoCss() |
||||
], |
||||
css: { |
||||
preprocessorOptions: { |
||||
scss: { |
||||
// 取消sass废弃API的报警
|
||||
silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'] |
||||
} |
||||
} |
||||
}, |
||||
resolve: { |
||||
alias: { |
||||
'@': resolve(__dirname, 'src') |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
Loading…
Reference in new issue