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 @@ |
|||||||
|
# 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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
.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 @@ |
|||||||
|
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 @@ |
|||||||
|
# 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 @@ |
|||||||
|
# 依赖 |
||||||
|
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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
# 闸口数据管理小程序 |
||||||
|
|
||||||
|
基于 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 @@ |
|||||||
|
<!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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
/// <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 @@ |
|||||||
|
/** |
||||||
|
* 用户相关接口 |
||||||
|
*/ |
||||||
|
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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
// 闸口数据常量和假数据
|
||||||
|
|
||||||
|
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 @@ |
|||||||
|
export const APP_NAME = 'msmg-uni' |
||||||
|
export const APP_VERSION = 'v1.0.0' |
||||||
|
export const APP_ICP = '' |
||||||
@ -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 @@ |
|||||||
|
<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 @@ |
|||||||
|
/// <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 @@ |
|||||||
|
export const useToast = (content: string) => { |
||||||
|
if (!content) return |
||||||
|
uni.showToast({ |
||||||
|
title: content, |
||||||
|
icon: 'none' |
||||||
|
}) |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
<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 @@ |
|||||||
|
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 @@ |
|||||||
|
.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 @@ |
|||||||
|
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 @@ |
|||||||
|
import { createPinia } from 'pinia' |
||||||
|
|
||||||
|
const pinia = createPinia() |
||||||
|
|
||||||
|
export default pinia |
||||||
@ -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 @@ |
|||||||
|
/** |
||||||
|
* 这里是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 @@ |
|||||||
|
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 @@ |
|||||||
|
{ |
||||||
|
"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 @@ |
|||||||
|
// 闸口站点相关类型定义
|
||||||
|
|
||||||
|
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 @@ |
|||||||
|
declare namespace SignDto { |
||||||
|
/** |
||||||
|
* 登录 |
||||||
|
*/ |
||||||
|
interface LoginDto { |
||||||
|
uname: string; |
||||||
|
pwd: string; |
||||||
|
} |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
// 闸口数据相关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 @@ |
|||||||
|
declare namespace SignVo { |
||||||
|
interface LoginVo { |
||||||
|
accessToken: string |
||||||
|
} |
||||||
|
} |
||||||
@ -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 @@ |
|||||||
|
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