Browse Source

feat: 闸口管理系统 - 首次提交

- 初始化项目结构,基于 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
LeoAnn 5 days ago
parent
commit
e01308fa0a
  1. 190
      .claude/CLAUDE.md
  2. 16
      .claude/settings.local.json
  3. 1
      .env
  4. 14
      .eslintignore
  5. 177
      .eslintrc.js
  6. 21
      .gitignore
  7. 47
      .prettierignore
  8. 14
      .prettierrc
  9. 149
      README_GATE.md
  10. 20
      index.html
  11. 88
      package.json
  12. 10341
      pnpm-lock.yaml
  13. 10
      shims-uni.d.ts
  14. 13
      src/@api/user.api.ts
  15. 48
      src/@layout/PageTitle.vue
  16. 16
      src/@layout/RootView.vue
  17. 23
      src/App.vue
  18. 153
      src/common/gate-data.ts
  19. 3
      src/common/global.ts
  20. 32
      src/components/PageBack.vue
  21. 9
      src/components/StatusBar.vue
  22. 8
      src/env.d.ts
  23. 7
      src/hooks/toast.use.ts
  24. 13
      src/main.ts
  25. 76
      src/manifest.json
  26. 58
      src/pages.json
  27. 260
      src/pages/gate/detail.vue
  28. 487
      src/pages/gate/history.vue
  29. 368
      src/pages/gate/list.vue
  30. 616
      src/pages/gate/upload.vue
  31. 490
      src/pages/index/index.vue
  32. 70
      src/pages/test/test.vue
  33. 6
      src/shime-uni.d.ts
  34. BIN
      src/static/logo.png
  35. 27
      src/static/styles/theme.css
  36. 266
      src/stores/gate.store.ts
  37. 5
      src/stores/index.ts
  38. 21
      src/stores/theme.store.ts
  39. 76
      src/uni.scss
  40. 120
      src/utils/http.ts
  41. 38
      tsconfig.json
  42. 33
      types/dto/gate.dto.ts
  43. 9
      types/dto/sign.d.ts
  44. 25
      types/http.d.ts
  45. 35
      types/vo/gate.vo.ts
  46. 5
      types/vo/sign.d.ts
  47. 79
      uno.config.ts
  48. 28
      vite.config.ts

190
.claude/CLAUDE.md

@ -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"`

16
.claude/settings.local.json

@ -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": []
}
}

1
.env

@ -0,0 +1 @@ @@ -0,0 +1 @@
VITE_BASE_URL=http://localhost:3000

14
.eslintignore

@ -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/

177
.eslintrc.js

@ -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'
}
}

21
.gitignore vendored

@ -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?

47
.prettierignore

@ -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

14
.prettierrc

@ -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
}

149
README_GATE.md

@ -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

20
index.html

@ -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>

88
package.json

@ -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"
}
}

10341
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

10
shims-uni.d.ts vendored

@ -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 {
}
}

13
src/@api/user.api.ts

@ -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'
})

48
src/@layout/PageTitle.vue

@ -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>

16
src/@layout/RootView.vue

@ -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>

23
src/App.vue

@ -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>

153
src/common/gate-data.ts

@ -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

3
src/common/global.ts

@ -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 = ''

32
src/components/PageBack.vue

@ -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>

9
src/components/StatusBar.vue

@ -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>

8
src/env.d.ts vendored

@ -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
}

7
src/hooks/toast.use.ts

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
export const useToast = (content: string) => {
if (!content) return
uni.showToast({
title: content,
icon: 'none'
})
}

13
src/main.ts

@ -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
}
}

76
src/manifest.json

@ -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"
}

58
src/pages.json

@ -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"
},
}

260
src/pages/gate/detail.vue

@ -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">/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 {
// storeAPI
// 使
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>

487
src/pages/gate/history.vue

@ -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">/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>

368
src/pages/gate/list.vue

@ -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>

616
src/pages/gate/upload.vue

@ -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">/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>

490
src/pages/index/index.vue

@ -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>

70
src/pages/test/test.vue

@ -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>

6
src/shime-uni.d.ts vendored

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
export {}
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

BIN
src/static/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

27
src/static/styles/theme.css

@ -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;
}

266
src/stores/gate.store.ts

@ -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
}
}
})

5
src/stores/index.ts

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

21
src/stores/theme.store.ts

@ -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
}
})

76
src/uni.scss

@ -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;

120
src/utils/http.ts

@ -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 }

38
tsconfig.json

@ -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"
]
}

33
types/dto/gate.dto.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
}

9
types/dto/sign.d.ts vendored

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
declare namespace SignDto {
/**
*
*/
interface LoginDto {
uname: string;
pwd: string;
}
}

25
types/http.d.ts vendored

@ -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>>
}

35
types/vo/gate.vo.ts

@ -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
}

5
types/vo/sign.d.ts vendored

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
declare namespace SignVo {
interface LoginVo {
accessToken: string
}
}

79
uno.config.ts

@ -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'
}
]
})

28
vite.config.ts

@ -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…
Cancel
Save