فهرست منبع

vue2升级vue3适配鸿蒙初步版本

jiuling 3 ماه پیش
والد
کامیت
78e7d651fc
76فایلهای تغییر یافته به همراه21078 افزوده شده و 9635 حذف شده
  1. 8 0
      .env
  2. 4 0
      .env.development
  3. 4 0
      .env.production
  4. 4 0
      .gitignore
  5. 77 58
      .kiro/specs/vue2-to-vue3-migration/tasks.md
  6. 77 55
      App.vue
  7. 1 0
      api/services/agriculturalMachines.js
  8. 119 114
      components/common/LocationPicker.vue
  9. 65 66
      components/common/dict-tag.vue
  10. 409 328
      components/common/jessibuca.vue
  11. 254 235
      components/common/video-player.vue
  12. 31 16
      config/api.js
  13. 1 0
      index.html
  14. 20 0
      jsconfig.json
  15. 23 38
      main.js
  16. 27 0
      manifest.json
  17. 4506 1
      package-lock.json
  18. 38 26
      package.json
  19. 8 3
      pages.json
  20. 28 33
      pages/about/index.vue
  21. 853 0
      pages/activity/activity-detail-new.vue
  22. 420 458
      pages/activity/activity-detail.vue
  23. 1770 0
      pages/activity/activity-detail.vue.backup
  24. 207 216
      pages/activity/index.vue
  25. 96 98
      pages/chart/index.vue
  26. 454 413
      pages/dashboard/index.vue
  27. 1596 0
      pages/dashboard/index_back.vue
  28. 168 171
      pages/device/device-list/agricultural/index.vue
  29. 630 797
      pages/device/device-list/detail-camera.vue
  30. 193 258
      pages/device/device-list/detail-collector.vue
  31. 450 512
      pages/device/device-list/detail-machine.vue
  32. 362 363
      pages/device/device-list/index.vue
  33. 119 123
      pages/device/index.vue
  34. 12 1
      pages/device/job-create/index.vue
  35. 2305 0
      pages/device/job-create/index.vue.backup
  36. 434 440
      pages/device/job-detail/index.vue
  37. 713 648
      pages/knowledge/ai-chat/index.vue
  38. 0 0
      pages/knowledge/ai-chat/index_vue3.vue
  39. 324 326
      pages/knowledge/detail.vue
  40. 266 282
      pages/knowledge/index.vue
  41. 224 188
      pages/login/forget-password.vue
  42. 222 237
      pages/login/index.vue
  43. 203 170
      pages/login/register.vue
  44. 331 383
      pages/plots/list.vue
  45. 2 4
      pages/privacy/index.vue
  46. 10 17
      pages/service/certification.vue
  47. 228 231
      pages/service/expert-chat.vue
  48. 71 72
      pages/service/expert-detail.vue
  49. 121 129
      pages/service/expert.vue
  50. 10 16
      pages/service/insurance.vue
  51. 90 184
      pages/service/mall-detail.vue
  52. 131 217
      pages/service/mall.vue
  53. 174 196
      pages/service/my-publish.vue
  54. 290 309
      pages/service/purchase-publish.vue
  55. 296 310
      pages/service/sales-detail.vue
  56. 308 324
      pages/service/sales-publish.vue
  57. 145 161
      pages/service/sales.vue
  58. 104 110
      pages/settings/index.vue
  59. 162 165
      pages/user/index.vue
  60. 85 83
      pages/userInfo/index.vue
  61. 19 10
      store/index.js
  62. 11 0
      types/uview-plus.d.ts
  63. 1 2
      uni.scss
  64. 420 0
      utils/composables/useDict.js
  65. 44 19
      utils/filters.js
  66. 21 4
      utils/jessibuca-plugin.js
  67. 2 2
      utils/lib/request/adapters/index.js
  68. 5 5
      utils/lib/request/core/Request.js
  69. 1 1
      utils/lib/request/core/defaults.js
  70. 2 2
      utils/lib/request/core/mergeConfig.js
  71. 22 0
      utils/mixins/dictMixin.js
  72. 4 2
      utils/request.js
  73. 13 3
      utils/uuid.modified.js
  74. 71 0
      vite.config.js
  75. 9 0
      vue.config.js
  76. 150 0
      环境配置说明.md

+ 8 - 0
.env

@@ -0,0 +1,8 @@
+# 默认环境变量配置
+# 此文件会被 .env.development 和 .env.production 覆盖
+
+# API 基础地址
+VITE_BASE_URL=https://nxy.gbdfarm.com:9000/pro-uniapp
+
+# 上传地址
+VITE_UPLOAD_URL=https://nxy.gbdfarm.com

+ 4 - 0
.env.development

@@ -0,0 +1,4 @@
+# 开发环境配置
+# H5 开发环境使用 proxy 代理
+VITE_BASE_URL=/base
+VITE_UPLOAD_URL=http://nxy.gbdfarm.com

+ 4 - 0
.env.production

@@ -0,0 +1,4 @@
+# 生产环境配置
+# 生产环境使用真实域名
+VITE_BASE_URL=https://nxy.gbdfarm.com:9000/pro-uniapp
+VITE_UPLOAD_URL=https://nxy.gbdfarm.com

+ 4 - 0
.gitignore

@@ -10,3 +10,7 @@ node_modules/
 /unpackage/**
 /uni_modules/**
 .hbuilderx/launch.json
+
+# 环境变量文件(保留模板文件,忽略本地配置)
+.env.local
+.env.*.local

+ 77 - 58
.kiro/specs/vue2-to-vue3-migration/tasks.md

@@ -8,20 +8,21 @@
 
 ## Tasks
 
-- [-] 1. 环境准备和依赖升级
+- [x] 1. 环境准备和依赖升级
   - 创建新分支 `feature/vue3-migration`
   - 备份当前项目代码
   - 更新 package.json 中的依赖版本
   - 更新 manifest.json 配置
   - _Requirements: 4.1, 4.2, 4.4_
 
-- [ ]* 1.1 编写依赖升级验证测试
+- [x] 1.1 编写依赖升级验证测试
+
   - 验证 package.json 中 Vue 版本为 3.x
   - 验证 Vuex 版本为 4.x
   - 验证 uni-app 版本支持 Vue3
   - _Requirements: 4.1, 4.2, 4.4_
 
-- [ ] 2. 迁移入口文件 main.js
+- [x] 2. 迁移入口文件 main.js
   - 将 `new Vue()` 改为 `createSSRApp()`
   - 更新 Vuex store 的注册方式
   - 迁移全局过滤器为全局方法
@@ -30,49 +31,52 @@
   - 处理 Jessibuca 插件的条件编译
   - _Requirements: 9.1, 9.2, 4.3_
 
-- [ ]* 2.1 编写 main.js 迁移验证测试
+- [x] 2.1 编写 main.js 迁移验证测试
+
   - 验证使用 createSSRApp 创建应用
   - 验证全局属性正确注册
   - 验证插件正确加载
   - _Requirements: 9.1, 9.2_
 
-- [ ] 3. 迁移 Vuex Store
+- [x] 3. 迁移 Vuex Store
   - 将 `new Vuex.Store()` 改为 `createStore()`
   - 移除 `Vue.use(Vuex)`
   - 保持所有 state、getters、mutations、actions 的业务逻辑不变
   - _Requirements: 3.1, 3.4_
 
-- [ ]* 3.1 编写 Store 功能测试
+- [x] 3.1 编写 Store 功能测试
+
   - 测试 state 读取
   - 测试 mutations 提交
   - 测试 actions 调度
   - 验证业务逻辑一致性
   - _Requirements: 3.4_
 
-- [ ] 4. 迁移工具函数模块
+- [x] 4. 迁移工具函数模块
   - 检查 utils 目录下的所有工具函数
   - 移除 window/document 直接引用,替换为 uni-app API
   - 更新导出方式 (如需要)
   - _Requirements: 10.1, 10.2_
 
-- [ ]* 4.1 编写工具函数单元测试
+- [x] 4.1 编写工具函数单元测试
+
   - 测试日期格式化函数
   - 测试坐标转换函数
   - 测试存储工具函数
   - _Requirements: 10.1, 10.2_
 
-- [ ] 5. 迁移 API 服务模块
+- [x] 5. 迁移 API 服务模块
   - 检查 api/services 目录下的所有服务文件
   - 确保请求拦截器兼容 Vue3
   - 更新 store 引用方式 (如有)
   - _Requirements: 11.1_
 
-- [ ] 6. Checkpoint - 基础设施验证
+- [x] 6. Checkpoint - 基础设施验证
   - 确保所有基础设施迁移完成
   - 运行所有单元测试
   - 询问用户是否有问题
 
-- [ ] 7. 迁移公共组件 - dict-tag.vue
+- [x] 7. 迁移公共组件 - dict-tag.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换 props 为 defineProps()
@@ -83,17 +87,19 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 7.1, 12.1, 12.2_
 
-- [ ]* 7.1 编写 dict-tag 组件属性测试
+- [x] 7.1 编写 dict-tag 组件属性测试
+
   - **Property 1: Options API 完整转换**
   - **Validates: Requirements 1.1, 1.3**
 
-- [ ]* 7.2 编写 dict-tag 组件单元测试
+- [x] 7.2 编写 dict-tag 组件单元测试
+
   - 测试组件渲染
   - 测试 props 传递
   - 测试样式应用
   - _Requirements: 7.1_
 
-- [ ] 8. 迁移公共组件 - LocationPicker.vue
+- [x] 8. 迁移公共组件 - LocationPicker.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换 props 和 emits
@@ -104,17 +110,19 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 6.2, 7.1, 7.2, 10.2_
 
-- [ ]* 8.1 编写 LocationPicker 组件属性测试
+- [x] 8.1 编写 LocationPicker 组件属性测试
+
   - **Property 7: Props 和 Emits 声明**
   - **Validates: Requirements 7.1, 7.2, 7.3**
 
-- [ ]* 8.2 编写 LocationPicker 组件功能测试
+- [x] 8.2 编写 LocationPicker 组件功能测试
+
   - 测试地图初始化
   - 测试位置选择
   - 测试事件发出
   - _Requirements: 10.2_
 
-- [ ] 9. 迁移公共组件 - video-player.vue
+- [x] 9. 迁移公共组件 - video-player.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换 $refs 为 ref() 模板引用
@@ -123,11 +131,12 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 6.3_
 
-- [ ]* 9.1 编写 video-player 组件属性测试
+- [x] 9.1 编写 video-player 组件属性测试
+
   - **Property 8: 模板引用转换**
   - **Validates: Requirements 6.3**
 
-- [ ] 10. 迁移公共组件 - jessibuca.vue
+- [x] 10. 迁移公共组件 - jessibuca.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 添加 H5 平台条件编译
@@ -136,17 +145,18 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 10.5, 15.1_
 
-- [ ]* 10.1 编写 jessibuca 组件功能测试
+- [x] 10.1 编写 jessibuca 组件功能测试
+
   - 测试 H5 平台视频播放
   - 验证条件编译正确性
   - _Requirements: 15.1_
 
-- [ ] 11. Checkpoint - 公共组件验证
+- [x] 11. Checkpoint - 公共组件验证
   - 确保所有公共组件迁移完成
   - 运行所有组件测试
   - 询问用户是否有问题
 
-- [ ] 12. 迁移 App.vue
+- [x] 12. 迁移 App.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换 onLaunch、onShow、onHide 生命周期 (保持 uni-app 命名)
@@ -155,11 +165,12 @@
   - 保持 style 不变
   - _Requirements: 1.1, 1.4, 1.5, 2.5_
 
-- [ ]* 12.1 编写 App.vue 属性测试
+- [x] 12.1 编写 App.vue 属性测试
+
   - **Property 3: uni-app 生命周期保持不变**
   - **Validates: Requirements 2.5**
 
-- [ ] 13. 迁移登录页面 - pages/login/index.vue
+- [x] 13. 迁移登录页面 - pages/login/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换 data 为 ref/reactive
@@ -171,17 +182,19 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 3.2, 3.3, 12.1, 12.2_
 
-- [ ]* 13.1 编写登录页面属性测试
+- [x] 13.1 编写登录页面属性测试
+
   - **Property 6: Store 使用方式转换**
   - **Validates: Requirements 3.2, 3.3**
 
-- [ ]* 13.2 编写登录页面功能测试
+- [x] 13.2 编写登录页面功能测试
+
   - 测试登录表单提交
   - 测试表单验证
   - 测试登录成功跳转
   - _Requirements: 11.1, 11.3_
 
-- [ ] 14. 迁移登录页面 - pages/login/register.vue
+- [x] 14. 迁移登录页面 - pages/login/register.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -191,7 +204,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 3.2, 12.1, 12.2_
 
-- [ ] 15. 迁移登录页面 - pages/login/forget-password.vue
+- [x] 15. 迁移登录页面 - pages/login/forget-password.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -200,13 +213,13 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 12.1, 12.2_
 
-- [ ] 16. Checkpoint - 登录模块验证
+- [x] 16. Checkpoint - 登录模块验证
   - 确保所有登录相关页面迁移完成
   - 运行登录流程功能测试
   - 询问用户是否有问题
 
 
-- [ ] 17. 迁移首页 - pages/dashboard/index.vue
+- [x] 17. 迁移首页 - pages/dashboard/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换 data 为 ref/reactive (注意复杂对象使用 reactive)
@@ -219,18 +232,20 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 3.2, 3.3, 8.1, 12.1, 12.2_
 
-- [ ]* 17.1 编写首页属性测试
+- [x] 17.1 编写首页属性测试
+
   - **Property 9: 计算属性和侦听器转换**
   - **Validates: Requirements 8.1, 8.2, 8.4**
 
-- [ ]* 17.2 编写首页功能测试
+- [x] 17.2 编写首页功能测试
+
   - 测试数据加载
   - 测试地块切换
   - 测试图表渲染
   - 测试页面跳转
   - _Requirements: 11.1, 11.2, 11.3_
 
-- [ ] 18. 迁移农事活动页面 - pages/activity/index.vue
+- [x] 18. 迁移农事活动页面 - pages/activity/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -240,7 +255,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 3.2, 12.1, 12.2_
 
-- [ ] 19. 迁移农事活动详情页 - pages/activity/activity-detail.vue
+- [x] 19. 迁移农事活动详情页 - pages/activity/activity-detail.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -249,7 +264,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 20. 迁移设备监测页面 - pages/device/index.vue
+- [x] 20. 迁移设备监测页面 - pages/device/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -259,7 +274,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 3.2, 12.1, 12.2_
 
-- [ ] 21. 迁移设备列表页面 - pages/device/device-list/index.vue
+- [x] 21. 迁移设备列表页面 - pages/device/device-list/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -268,7 +283,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 22. 迁移设备详情页面 (3个)
+- [x] 22. 迁移设备详情页面 (3个)
   - 迁移 pages/device/device-list/detail-camera.vue
   - 迁移 pages/device/device-list/detail-collector.vue
   - 迁移 pages/device/device-list/detail-machine.vue
@@ -280,7 +295,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 23. 迁移农机设备页面 - pages/device/device-list/agricultural/index.vue
+- [x] 23. 迁移农机设备页面 - pages/device/device-list/agricultural/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -289,7 +304,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 24. 迁移作业相关页面 (2个)
+- [x] 24. 迁移作业相关页面 (2个)
   - 迁移 pages/device/job-create/index.vue
   - 迁移 pages/device/job-detail/index.vue
   - 将 Options API 转换为 Composition API
@@ -300,12 +315,12 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 25. Checkpoint - 设备模块验证
+- [x] 25. Checkpoint - 设备模块验证
   - 确保所有设备相关页面迁移完成
   - 运行设备模块功能测试
   - 询问用户是否有问题
 
-- [ ] 26. 迁移农业知识页面 - pages/knowledge/index.vue
+- [x] 26. 迁移农业知识页面 - pages/knowledge/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -314,16 +329,16 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 27. 迁移知识详情页面 - pages/knowledge/detail.vue
+- [x] 27. 迁移知识详情页面 - pages/knowledge/detail.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
-  - 转换生命周期钩子
+  - 转换生命周期钩子         
   - 移除所有 this 引用
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 28. 迁移 AI 聊天页面 - pages/knowledge/ai-chat/index.vue
+- [x] 28. 迁移 AI 聊天页面 - pages/knowledge/ai-chat/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -332,7 +347,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 29. 迁移用户中心页面 - pages/user/index.vue
+- [x] 29. 迁移用户中心页面 - pages/user/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -342,7 +357,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 3.2, 12.1, 12.2_
 
-- [ ] 30. 迁移用户信息页面 - pages/userInfo/index.vue
+- [x] 30. 迁移用户信息页面 - pages/userInfo/index.vue
   - 将 Options API 转换为 Composition API
   - 使用 `<script setup>` 语法
   - 转换响应式数据和方法
@@ -352,7 +367,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 3.2, 12.1, 12.2_
 
-- [ ] 31. 迁移其他页面 (10个)
+- [x] 31. 迁移其他页面 (10个)
   - 迁移 pages/machine/index.vue
   - 迁移 pages/field/index.vue
   - 迁移 pages/plots/list.vue
@@ -369,7 +384,7 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 32. 迁移服务页面 (10个)
+- [x] 32. 迁移服务页面 (10个)
   - 迁移 pages/service/mall.vue
   - 迁移 pages/service/mall-detail.vue
   - 迁移 pages/service/sales.vue
@@ -390,37 +405,40 @@
   - 保持 template 和 style 不变
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.5, 12.1, 12.2_
 
-- [ ] 33. Checkpoint - 所有页面迁移完成
+- [x] 33. Checkpoint - 所有页面迁移完成
   - 确保所有页面组件迁移完成
   - 运行所有页面测试
   - 询问用户是否有问题
 
-- [ ] 34. 添加 HarmonyOS 平台支持
+- [x] 34. 添加 HarmonyOS 平台支持
   - 在所有条件编译中添加 HarmonyOS 支持
   - 检查并更新平台特定代码
   - 确保使用 uni-app 跨平台 API
   - _Requirements: 10.3, 10.5, 13.5_
 
-- [ ]* 34.1 编写 HarmonyOS 兼容性属性测试
+- [x] 34.1 编写 HarmonyOS 兼容性属性测试
+
   - **Property 10: 浏览器 API 移除**
   - **Validates: Requirements 10.1, 10.2**
 
-- [ ]* 34.2 编写 HarmonyOS 兼容性属性测试
+- [x] 34.2 编写 HarmonyOS 兼容性属性测试
+
   - **Property 11: 条件编译正确使用**
   - **Validates: Requirements 10.5, 13.5**
 
-- [ ] 35. 代码质量检查和优化
+- [x] 35. 代码质量检查和优化
   - 运行 ESLint 检查所有迁移后的代码
   - 修复所有 ESLint 错误和警告
   - 检查所有 TODO 注释,确保有明确说明
   - 优化代码结构和命名
   - _Requirements: 14.1, 14.2, 14.3, 14.5, 17.2_
 
-- [ ]* 35.1 编写代码质量属性测试
+- [x] 35.1 编写代码质量属性测试
+
   - **Property 17: TODO 注释添加**
   - **Validates: Requirements 14.2, 17.1, 17.2**
 
-- [ ] 36. 编写综合属性测试
+- [x] 36. 编写综合属性测试
   - 编写 Property 1: Options API 完整转换测试
   - 编写 Property 2: 生命周期钩子正确映射测试
   - 编写 Property 4: this 关键字完全移除测试
@@ -433,7 +451,7 @@
   - 配置每个测试运行 100 次迭代
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 5.1, 5.2, 5.3, 5.4, 12.1, 12.2_
 
-- [ ] 37. 跨平台功能测试
+- [x] 37. 跨平台功能测试
   - 在 Android 平台测试核心功能
   - 在 iOS 平台测试核心功能
   - 在 H5 平台测试核心功能
@@ -441,7 +459,8 @@
   - 验证所有平台行为一致
   - _Requirements: 13.1, 13.2, 13.3, 13.4_
 
-- [ ]* 37.1 编写跨平台功能测试用例
+- [x] 37.1 编写跨平台功能测试用例
+
   - 测试用户登录流程
   - 测试数据列表加载
   - 测试表单提交
@@ -480,7 +499,7 @@
   - 记录已知问题和 TODO 项
   - _Requirements: 14.4, 20.1, 20.2, 20.3_
 
-- [ ] 41. 最终 Checkpoint - 迁移完成
+- [x] 41. 最终 Checkpoint - 迁移完成
   - 确保所有任务完成
   - 确保所有测试通过
   - 询问用户是否准备合并代码

+ 77 - 55
App.vue

@@ -1,69 +1,91 @@
-<script>
+<script setup>
 import storage from "@/utils/storage.js";
-export default {
-  onLaunch: function() {
-    console.log('App Launch');
-    this.checkLoginStatus();
-    // 添加全局页面生命周期监听
-    // uni.onPageNotFound(function(e) {
-    //   console.error('页面不存在:', e);
-    //   uni.navigateTo({
-    //     url: '/pages/dashboard/index'
-    //   });
-    // });
-  },
-  onShow: function() {
-    console.log('App Show');
-  },
-  onHide: function() {
-    console.log('App Hide');
-  },
-  methods: {
-    checkLoginStatus() {
-      // 登录状态检查,对敏感页面进行拦截
-      const pages = ['pages/device/index']; 
+import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
+import { useStore } from 'vuex';
+import { getCurrentInstance } from 'vue';
+
+const store = useStore();
+const instance = getCurrentInstance();
+
+// 登录状态检查函数
+const checkLoginStatus = () => {
+  // 登录状态检查,对敏感页面进行拦截
+  const pages = ['pages/device/index']; 
+  
+  uni.addInterceptor('navigateTo', {
+    invoke(e) {
+      const url = e.url;
+      // 检查是否属于需要登录的页面
+      const needLogin = pages.some(page => url.indexOf(page) > -1);
       
-      uni.addInterceptor('navigateTo', {
-        invoke(e) {
-          const url = e.url;
-          // 检查是否属于需要登录的页面
-          const needLogin = pages.some(page => url.indexOf(page) > -1);
-          
-          if (needLogin && !storage.isLoggedIn()) {
-            uni.navigateTo({
-              url: '/pages/login/index'
-            });
-            return false;
-          }
-          return true;
-        }
-      });
+      if (needLogin && !storage.isLoggedIn()) {
+        uni.navigateTo({
+          url: '/pages/login/index'
+        });
+        return false;
+      }
+      return true;
+    }
+  });
+  
+  uni.addInterceptor('switchTab', {
+    invoke(e) {
+      const url = e.url;
+      // 检查是否属于需要登录的页面
+      const needLogin = pages.some(page => url.indexOf(page) > -1);
       
-      uni.addInterceptor('switchTab', {
-        invoke(e) {
-          const url = e.url;
-          // 检查是否属于需要登录的页面
-          const needLogin = pages.some(page => url.indexOf(page) > -1);
-          
-          if (needLogin && !storage.isLoggedIn()) {
-            uni.navigateTo({
-              url: '/pages/login/index'
-            });
-            return false;
-          }
-          return true;
-        }
-      });
+      if (needLogin && !storage.isLoggedIn()) {
+        uni.navigateTo({
+          url: '/pages/login/index'
+        });
+        return false;
+      }
+      return true;
+    }
+  });
+};
+
+// 初始化其他插件(uview-plus 已在 main.js 中注册)
+const initPlugins = async () => {
+  try {
+    // #ifdef H5
+    // H5 环境加载 jessibuca 插件
+    if (typeof window !== 'undefined') {
+      const jessibucaModule = await import('./utils/jessibuca-plugin');
+      if (instance && instance.appContext && instance.appContext.app) {
+        instance.appContext.app.use(jessibucaModule.default);
+      }
     }
+    // #endif
+  } catch (error) {
+    console.error('插件初始化失败:', error);
   }
-}
+};
+
+// uni-app 生命周期钩子 (保持原有命名)
+onLaunch(() => {
+  console.log('App Launch');
+  // 初始化插件
+  initPlugins();
+  // 初始化 store 状态
+  store.dispatch('init');
+  checkLoginStatus();
+});
+
+onShow(() => {
+  console.log('App Show');
+});
+
+onHide(() => {
+  console.log('App Hide');
+});
 </script>
 
 <!-- App.vue中不需要template,uni-app会自动处理页面跳转 -->
 
 <style lang="scss">
 /* 全局基础样式 */
-@import "uview-ui/index.scss";
+@import "uview-plus/index.scss";
 page {
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
   font-size: 28rpx;

+ 1 - 0
api/services/agriculturalMachines.js

@@ -12,6 +12,7 @@ const userInfo = storage.getUserInfo()
  * @returns {Promise} 设备列表
  */
 export function machinesDeviceList(params = {}) {
+	console.log("userInfo",userInfo);
   if (userInfo.sysUser.deptId) params.deptIdList = userInfo.sysUser.deptId;
   return http.request({
     url: 'base/machines/list',

+ 119 - 114
components/common/LocationPicker.vue

@@ -20,7 +20,7 @@
         suffix-icon="arrow-down"
       >
         <!-- 清除按钮 -->
-        <template slot="suffix">
+        <template #suffix>
           <view 
             v-if="innerValue"
             @click.stop="clearAddress"
@@ -39,131 +39,136 @@
   <!-- </view> -->
 </template>
 
-<script>
-import cityRows from '@/utils/data.json';
-
-export default {
-  name: "LocationPicker",
-  props: {
-	mode: { // 新增模式属性
-	  type: String,
-	  default: "edit" // edit / view
-	},
-    value: { // v-model
-      type: [String, Number],
-      default: ""
-    },
-    label: { // 左侧文字
-      type: String,
-      default: "所在地"
-    },
-    placeholder: {
-      type: String,
-      default: "请选择省市区"
-    },
-    popupTitle: {
-      type: String,
-      default: "请选择省市区"
-    },
-    required: {
-      type: Boolean,
-      default: false
-    }
+<script setup>
+import { ref, watch } from 'vue'
+import cityRows from '@/utils/data.json'
+
+// Props definition
+const props = defineProps({
+  mode: { // 新增模式属性
+    type: String,
+    default: "edit" // edit / view
   },
-  data() {
-    return {
-      innerValue: this.value,
-      localData: [] // 省市区树数据
-    }
+  value: { // v-model
+    type: [String, Number],
+    default: ""
   },
-  watch: {
-    value(newVal) {
-      this.innerValue = newVal
-    },
-    innerValue(newVal) {
-      this.$emit("input", newVal)
-    }
+  label: { // 左侧文字
+    type: String,
+    default: "所在地"
   },
-  created() {
-    this.localData = this.get_city_tree()
+  placeholder: {
+    type: String,
+    default: "请选择省市区"
   },
-  methods: {
-    /** 点击选择后的回调 */
-    onChange(e) {
-      const lastNode = e.detail.value[e.detail.value.length - 1]
-      this.innerValue = lastNode.value // 只存最底层的 code
-    },
-    /** 清空地址 */
-    clearAddress() {
-      this.innerValue = ""
-      this.$emit("clear")
-    },
-    /** 回显文字(递归找路径) */
-    getLocationLabel(value) {
-      if (!value) return ""
-      let label = ""
-      const traverse = (nodes) => {
-        for (const node of nodes) {
-          if (node.value === value) {
-            label = node.text
-            return true
-          }
-          if (node.children && traverse(node.children)) {
-            label = node.text + " - " + label
-            return true
-          }
-        }
-        return false
-      }
-      traverse(this.localData)
-      return label
-    },
-    /** 生成树数据 */
-    get_city_tree() {
-      let res = []
-      if (cityRows.length) {
-        res = this.handleTree(cityRows)
+  popupTitle: {
+    type: String,
+    default: "请选择省市区"
+  },
+  required: {
+    type: Boolean,
+    default: false
+  }
+})
+
+// Emits definition
+const emit = defineEmits(['input', 'clear'])
+
+// Reactive data
+const innerValue = ref(props.value)
+const localData = ref([]) // 省市区树数据
+
+// Watchers
+watch(() => props.value, (newVal) => {
+  innerValue.value = newVal
+})
+
+watch(innerValue, (newVal) => {
+  emit("input", newVal)
+})
+
+// Methods
+/** 点击选择后的回调 */
+const onChange = (e) => {
+  const lastNode = e.detail.value[e.detail.value.length - 1]
+  innerValue.value = lastNode.value // 只存最底层的 code
+}
+
+/** 清空地址 */
+const clearAddress = () => {
+  innerValue.value = ""
+  emit("clear")
+}
+
+/** 回显文字(递归找路径) */
+const getLocationLabel = (value) => {
+  if (!value) return ""
+  let label = ""
+  const traverse = (nodes) => {
+    for (const node of nodes) {
+      if (node.value === value) {
+        label = node.text
+        return true
       }
-      return res
-    },
-    /** 递归组装树 */
-    handleTree(data, parent_code = null) {
-      let res = []
-      let keys = {
-        id: "code",
-        pid: "parent_code",
-        children: "children",
-        text: "name",
-        value: "code"
+      if (node.children && traverse(node.children)) {
+        label = node.text + " - " + label
+        return true
       }
+    }
+    return false
+  }
+  traverse(localData.value)
+  return label
+}
+
+/** 生成树数据 */
+const get_city_tree = () => {
+  let res = []
+  if (cityRows.length) {
+    res = handleTree(cityRows)
+  }
+  return res
+}
+
+/** 递归组装树 */
+const handleTree = (data, parent_code = null) => {
+  let res = []
+  let keys = {
+    id: "code",
+    pid: "parent_code",
+    children: "children",
+    text: "name",
+    value: "code"
+  }
 
-      for (let item of data) {
-        if (parent_code === null) {
-          // 顶级
-          if (!item.hasOwnProperty(keys.pid) || item[keys.pid] == parent_code) {
-            let node = {
-              text: item[keys.text],
-              value: item[keys.value],
-              children: this.handleTree(data, item[keys.id])
-            }
-            res.push(node)
-          }
-        } else {
-          // 子级
-          if (item.hasOwnProperty(keys.pid) && item[keys.pid] == parent_code) {
-            let node = {
-              text: item[keys.text],
-              value: item[keys.value],
-              children: this.handleTree(data, item[keys.id])
-            }
-            res.push(node)
-          }
+  for (let item of data) {
+    if (parent_code === null) {
+      // 顶级
+      if (!item.hasOwnProperty(keys.pid) || item[keys.pid] == parent_code) {
+        let node = {
+          text: item[keys.text],
+          value: item[keys.value],
+          children: handleTree(data, item[keys.id])
         }
+        res.push(node)
+      }
+    } else {
+      // 子级
+      if (item.hasOwnProperty(keys.pid) && item[keys.pid] == parent_code) {
+        let node = {
+          text: item[keys.text],
+          value: item[keys.value],
+          children: handleTree(data, item[keys.id])
+        }
+        res.push(node)
       }
-      return res
     }
   }
+  return res
 }
+
+// Initialize data (replaces created lifecycle)
+localData.value = get_city_tree()
 </script>
 
 <style scoped>

+ 65 - 66
components/common/dict-tag.vue

@@ -2,77 +2,76 @@
   <view class="dict-tag" :class="tagClass">{{ label }}</view>
 </template>
 
-<script>
-import dictMixin from '@/utils/mixins/dictMixin';
-
-export default {
-  name: 'DictTag',
-  mixins: [dictMixin],
-  props: {
-    // 字典类型
-    dictType: {
-      type: String,
-      required: true
-    },
-    // 字典值
-    value: {
-      type: [String, Number],
-      required: true
-    },
-    // 是否带颜色样式
-    colored: {
-      type: Boolean,
-      default: true
-    }
-  },
-  data() {
-    return {
-      dictTypeList: [] // 将通过created动态设置
-    };
+<script setup>
+import { computed, getCurrentInstance, onMounted } from 'vue'
+import { useDict } from '@/utils/composables/useDict'
+
+const props = defineProps({
+  // 字典类型
+  dictType: {
+    type: String,
+    required: true
   },
-  computed: {
-    // 字典标签
-    label() {
-      return this.getDictLabel(this.dictType, this.value, '');
-    },
-    // 标签样式类
-    tagClass() {
-      if (!this.colored) return '';
-      const className = this.getDictClass(this.dictType, this.value, '');
-      return className ? `tag-${className}` : '';
-    }
+  // 字典值
+  value: {
+    type: [String, Number],
+    required: true
   },
-  created() {
-    // 检查父组件是否已加载此字典
-    let parentWithDict = this.findParentWithDict(this.dictType);
-    
-    if (parentWithDict) {
-      // 父组件已加载此字典,使用父组件的字典数据
-      if (parentWithDict.dictData && parentWithDict.dictData[this.dictType]) {
-        this.$set(this.dictData, this.dictType, parentWithDict.dictData[this.dictType]);
-      }
-    } else {
-      // 父组件未加载此字典,自己加载
-      this.dictTypeList = [this.dictType];
-      this.loadDict();
+  // 是否带颜色样式
+  colored: {
+    type: Boolean,
+    default: true
+  }
+})
+
+// 获取当前组件实例,用于访问父组件
+const instance = getCurrentInstance()
+
+// 初始化字典功能,但不自动加载
+const { dictData, loadDict, getDictLabel, getDictClass } = useDict([], { autoLoad: false })
+
+// 查找已加载指定字典类型的父组件
+const findParentWithDict = (dictType) => {
+  let parent = instance.parent
+  while (parent) {
+    // 检查父组件是否有 dictData 并且包含指定类型的字典
+    if (parent.ctx && parent.ctx.dictData && parent.ctx.dictData[dictType]) {
+      console.log(`[DictTag] Found parent with dictionary ${dictType} already loaded`)
+      return parent.ctx
     }
-  },
-  methods: {
-    // 查找已加载指定字典类型的父组件
-    findParentWithDict(dictType) {
-      let parent = this.$parent;
-      while (parent) {
-        if (parent.dictData && parent.dictData[dictType]) {
-          console.log(`[DictTag] Found parent with dictionary ${dictType} already loaded`);
-          return parent;
-        }
-        parent = parent.$parent;
-      }
-      console.log(`[DictTag] No parent found with dictionary ${dictType}, loading it independently`);
-      return null;
+    parent = parent.parent
+  }
+  console.log(`[DictTag] No parent found with dictionary ${dictType}, loading it independently`)
+  return null
+}
+
+// 计算属性:字典标签
+const label = computed(() => {
+  return getDictLabel(props.dictType, props.value, '')
+})
+
+// 计算属性:标签样式类
+const tagClass = computed(() => {
+  if (!props.colored) return ''
+  const className = getDictClass(props.dictType, props.value, '')
+  return className ? `tag-${className}` : ''
+})
+
+// 组件挂载时加载字典
+onMounted(() => {
+  // 检查父组件是否已加载此字典
+  const parentWithDict = findParentWithDict(props.dictType)
+  
+  if (parentWithDict) {
+    // 父组件已加载此字典,使用父组件的字典数据
+    if (parentWithDict.dictData && parentWithDict.dictData[props.dictType]) {
+      dictData[props.dictType] = parentWithDict.dictData[props.dictType]
     }
+  } else {
+    // 父组件未加载此字典,自己加载
+    loadDict([props.dictType])
   }
-};
+})
 </script>
 
 <style scoped>

+ 409 - 328
components/common/jessibuca.vue

@@ -31,351 +31,432 @@
   </div>
 </template>
 
-<script>
+<!-- #ifdef H5 -->
+<script setup>
+import { ref, watch, nextTick, onMounted, onBeforeUnmount, getCurrentInstance } from 'vue'
+import { useRoute } from 'vue-router'
+
+// Props
+const props = defineProps({
+  videoUrl: String,
+  error: String,
+  hasAudio: Boolean,
+  height: [String, Number]
+})
+
+// 获取当前实例 UID (用于管理多个播放器实例)
+const instance = getCurrentInstance()
+const uid = instance.uid
+
+// 获取路由
+const route = useRoute()
+
+// 模板引用
+const container = ref(null)
+
+// 响应式数据
+const playing = ref(false)
+const isNotMute = ref(false)
+const quieting = ref(false)
+const fullscreen = ref(false)
+const loaded = ref(false)
+const speed = ref(0)
+const performance = ref('')
+const kBps = ref(0)
+const btnDom = ref(null)
+const videoInfo = ref(null)
+const volume = ref(1)
+const rotate = ref(0)
+const vod = ref(true)
+const forceNoOffscreen = ref(false)
+const playerWidth = ref(0)
+const playerHeight = ref(0)
+const parentNodeResizeObserver = ref(null)
+
+// 全局播放器实例存储
 const jessibucaPlayer = {}
-export default {
-  name: 'Jessibuca',
-  props: ['videoUrl', 'error', 'hasAudio', 'height'],
-  data() {
-    return {
-      playing: false,
-      isNotMute: false,
-      quieting: false,
-      fullscreen: false,
-      loaded: false, // mute
-      speed: 0,
-      performance: '', // 工作情况
-      kBps: 0,
-      btnDom: null,
-      videoInfo: null,
-      volume: 1,
-      rotate: 0,
-      vod: true, // 点播
-      forceNoOffscreen: false
+
+// 判断是否处于全屏状态
+const isFullscreen = () => {
+  // #ifdef H5
+  return document.fullscreenElement ||
+    document.msFullscreenElement ||
+    document.mozFullScreenElement ||
+    document.webkitFullscreenElement || false
+  // #endif
+  // #ifndef H5
+  return false
+  // #endif
+}
+
+// 更新播放器 DOM 尺寸
+const updatePlayerDomSize = () => {
+  const dom = container.value
+  if (!dom) return
+
+  if (!parentNodeResizeObserver.value) {
+    // #ifdef H5
+    parentNodeResizeObserver.value = new ResizeObserver(() => {
+      updatePlayerDomSize()
+    })
+    parentNodeResizeObserver.value.observe(dom.parentNode)
+    // #endif
+  }
+  
+  // 获取父容器尺寸
+  const boxWidth = dom.parentNode.clientWidth
+  const boxHeight = dom.parentNode.clientHeight
+  
+  // 检查是否处于全屏状态
+  const isFullscreenState = isFullscreen()
+  let width, height
+  
+  if (isFullscreenState) {
+    // 全屏模式,使用窗口尺寸
+    // #ifdef H5
+    width = window.innerWidth
+    height = window.innerHeight
+    // #endif
+  } else {
+    // 非全屏模式,使用16:9比例
+    width = boxWidth
+    height = (9 / 16) * width
+    
+    // 如果计算出的高度超过容器高度,则以容器高度为基准重新计算宽度
+    if (boxHeight > 0 && height > boxHeight) {
+      height = boxHeight
+      width = height * 16 / 9
     }
-  },
-  watch: {
-    videoUrl: {
-      handler(val, _) {
-        this.$nextTick(() => {
-          this.play(val)
-        })
-      },
-      immediate: true
+  }
+  
+  // 限制尺寸不超过视口
+  // #ifdef H5
+  const clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight)
+  if (!isFullscreenState && height > clientHeight) {
+    height = clientHeight
+    width = (16 / 9) * height
+  }
+  // #endif
+  
+  playerWidth.value = width
+  playerHeight.value = height
+  
+  // 应用尺寸到容器
+  dom.style.width = `${width}px`
+  dom.style.height = `${height}px`
+  
+  // 如果播放器存在,更新播放器尺寸
+  if (playing.value && jessibucaPlayer[uid]) {
+    jessibucaPlayer[uid].resize(width, height)
+  }
+}
+
+// 公共方法:调整大小
+const resize = () => {
+  updatePlayerDomSize()
+}
+
+// 创建播放器
+const create = () => {
+  const options = {
+    container: container.value,
+    autoWasm: true,
+    background: '',
+    controlAutoHide: false,
+    debug: false,
+    decoder: 'static/js/jessibuca/decoder.js',
+    forceNoOffscreen: false,
+    hasAudio: typeof props.hasAudio === 'undefined' ? true : props.hasAudio,
+    heartTimeout: 5,
+    heartTimeoutReplay: true,
+    heartTimeoutReplayTimes: 3,
+    hiddenAutoPause: false,
+    hotKey: true,
+    isFlv: false,
+    isFullResize: false,
+    isNotMute: isNotMute.value,
+    isResize: true,
+    keepScreenOn: true,
+    loadingText: '请稍等, 视频加载中......',
+    loadingTimeout: 10,
+    loadingTimeoutReplay: true,
+    loadingTimeoutReplayTimes: 3,
+    openWebglAlignment: false,
+    operateBtns: {
+      fullscreen: false,
+      screenshot: false,
+      play: false,
+      audio: false,
+      record: false
+    },
+    recordType: 'mp4',
+    rotate: 0,
+    showBandwidth: false,
+    supportDblclickFullscreen: false,
+    timeout: 10,
+    useMSE: true,
+    useWCS: false,
+    useWebFullScreen: true,
+    videoBuffer: 0.1,
+    wasmDecodeErrorReplay: true,
+    wcsUseVideoRender: true
+  }
+  console.log('Jessibuca -> options: ', options)
+  
+  // #ifdef H5
+  jessibucaPlayer[uid] = new window.Jessibuca({ ...options })
+
+  const jessibuca = jessibucaPlayer[uid]
+  
+  jessibuca.on('pause', () => {
+    playing.value = false
+  })
+  jessibuca.on('play', () => {
+    playing.value = true
+  })
+  jessibuca.on('fullscreen', (msg) => {
+    fullscreen.value = msg
+  })
+  jessibuca.on('mute', (msg) => {
+    isNotMute.value = !msg
+  })
+  jessibuca.on('performance', (perf) => {
+    let show = '卡顿'
+    if (perf === 2) {
+      show = '非常流畅'
+    } else if (perf === 1) {
+      show = '流畅'
     }
-  },
-  created() {
-    const paramUrl = decodeURIComponent(this.$route.params.url)
-    this.$nextTick(() => {
-      this.updatePlayerDomSize()
-      window.onresize = this.updatePlayerDomSize
-      if (typeof (this.videoUrl) === 'undefined') {
-        this.videoUrl = paramUrl
-      }
-      this.btnDom = document.getElementById('buttonsBox')
+    performance.value = show
+  })
+  jessibuca.on('kBps', (kbps) => {
+    kBps.value = Math.round(kbps)
+  })
+  jessibuca.on('videoInfo', (msg) => {
+    console.log('Jessibuca -> videoInfo: ', msg)
+  })
+  jessibuca.on('audioInfo', (msg) => {
+    console.log('Jessibuca -> audioInfo: ', msg)
+  })
+  jessibuca.on('error', (msg) => {
+    console.log('Jessibuca -> error: ', msg)
+  })
+  jessibuca.on('timeout', (msg) => {
+    console.log('Jessibuca -> timeout: ', msg)
+  })
+  jessibuca.on('loadingTimeout', (msg) => {
+    console.log('Jessibuca -> timeout: ', msg)
+  })
+  jessibuca.on('delayTimeout', (msg) => {
+    console.log('Jessibuca -> timeout: ', msg)
+  })
+  jessibuca.on('playToRenderTimes', (msg) => {
+    console.log('Jessibuca -> playToRenderTimes: ', msg)
+  })
+  // #endif
+}
+
+// 播放按钮点击
+const playBtnClick = () => {
+  play(props.videoUrl)
+}
+
+// 播放
+const play = (url) => {
+  console.log('Jessibuca -> url: ', url)
+  if (jessibucaPlayer[uid]) {
+    destroy()
+  }
+  create()
+  
+  // #ifdef H5
+  jessibucaPlayer[uid].on('play', () => {
+    playing.value = true
+    loaded.value = true
+    quieting.value = jessibucaPlayer[uid].quieting
+  })
+  if (jessibucaPlayer[uid].hasLoaded()) {
+    jessibucaPlayer[uid].play(url)
+  } else {
+    jessibucaPlayer[uid].on('load', () => {
+      jessibucaPlayer[uid].play(url)
     })
-  },
-  // mounted() {
-  //   const ro = new ResizeObserver(entries => {
-  //     entries.forEach(entry => {
-  //       this.updatePlayerDomSize()
-  //     });
-  //   });
-  //   ro.observe(this.$refs.container);
-  // },
-  mounted() {
-    this.updatePlayerDomSize()
-  },
-  destroyed() {
-    if (jessibucaPlayer[this._uid]) {
-      jessibucaPlayer[this._uid].destroy()
-    }
-    this.playing = false
-    this.loaded = false
-    this.performance = ''
-  },
-  methods: {
-    updatePlayerDomSize() {
-      const dom = this.$refs.container
-      if (!dom) return;
-
-      if (!this.parentNodeResizeObserver) {
-        this.parentNodeResizeObserver = new ResizeObserver(entries => {
-          this.updatePlayerDomSize()
-        })
-        this.parentNodeResizeObserver.observe(dom.parentNode)
-      }
-      
-      // 获取父容器尺寸
-      const boxWidth = dom.parentNode.clientWidth
-      const boxHeight = dom.parentNode.clientHeight
-      
-      // 检查是否处于全屏状态
-      const isFullscreen = this.isFullscreen();
-      let width, height;
+  }
+  // #endif
+}
+
+// 暂停
+const pause = () => {
+  // #ifdef H5
+  if (jessibucaPlayer[uid]) {
+    jessibucaPlayer[uid].pause()
+  }
+  // #endif
+  playing.value = false
+  performance.value = ''
+}
+
+// 截图
+const screenshot = () => {
+  // #ifdef H5
+  if (jessibucaPlayer[uid]) {
+    jessibucaPlayer[uid].screenshot()
+  }
+  // #endif
+}
+
+// 静音
+const mute = () => {
+  // #ifdef H5
+  if (jessibucaPlayer[uid]) {
+    jessibucaPlayer[uid].mute()
+  }
+  // #endif
+}
+
+// 取消静音
+const cancelMute = () => {
+  // #ifdef H5
+  if (jessibucaPlayer[uid]) {
+    jessibucaPlayer[uid].cancelMute()
+  }
+  // #endif
+}
+
+// 销毁播放器
+const destroy = () => {
+  // #ifdef H5
+  if (jessibucaPlayer[uid]) {
+    jessibucaPlayer[uid].destroy()
+  }
+  if (document.getElementById('buttonsBox') == null && btnDom.value) {
+    container.value.appendChild(btnDom.value)
+  }
+  jessibucaPlayer[uid] = null
+  // #endif
+  playing.value = false
+  performance.value = ''
+}
+
+// 全屏切换
+const fullscreenSwich = () => {
+  const isFull = isFullscreen()
+  
+  if (!isFull) {
+    // 进入全屏
+    try {
+      const containerEl = container.value
       
-      if (isFullscreen) {
-        // 全屏模式,使用窗口尺寸
-        width = window.innerWidth;
-        height = window.innerHeight;
+      // #ifdef H5
+      // 尝试使用HTML5全屏API
+      if (containerEl.requestFullscreen) {
+        containerEl.requestFullscreen()
+      } else if (containerEl.webkitRequestFullscreen) {
+        containerEl.webkitRequestFullscreen()
+      } else if (containerEl.msRequestFullscreen) {
+        containerEl.msRequestFullscreen()
+      } else if (containerEl.mozRequestFullScreen) {
+        containerEl.mozRequestFullScreen()
       } else {
-        // 非全屏模式,使用16:9比例
-        width = boxWidth
-        height = (9 / 16) * width
-        
-        // 如果计算出的高度超过容器高度,则以容器高度为基准重新计算宽度
-        if (boxHeight > 0 && height > boxHeight) {
-          height = boxHeight
-          width = height * 16 / 9
+        // 如果原生API不可用,使用Jessibuca的全屏API
+        if (jessibucaPlayer[uid]) {
+          jessibucaPlayer[uid].setFullscreen(true)
         }
       }
+      // #endif
       
-      // 限制尺寸不超过视口
-      const clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight)
-      if (!isFullscreen && height > clientHeight) {
-        height = clientHeight
-        width = (16 / 9) * height
-      }
-      
-      this.playerWidth = width
-      this.playerHeight = height
-      
-      // 应用尺寸到容器
-      dom.style.width = `${width}px`
-      dom.style.height = `${height}px`
-      
-      // 如果播放器存在,更新播放器尺寸
-      if (this.playing && jessibucaPlayer[this._uid]) {
-        jessibucaPlayer[this._uid].resize(width, height)
-      }
-    },
-    
-    // 公共方法:调整大小
-    resize() {
-      this.updatePlayerDomSize()
-    },
-    
-    // 判断是否处于全屏状态
-    isFullscreen() {
-      return document.fullscreenElement ||
-        document.msFullscreenElement ||
-        document.mozFullScreenElement ||
-        document.webkitFullscreenElement || false
-    },
-    create() {
-      const options = {
-        container: this.$refs.container,
-        autoWasm: true,
-        background: '',
-        controlAutoHide: false,
-        debug: false,
-        decoder: 'static/js/jessibuca/decoder.js',
-        forceNoOffscreen: false,
-        hasAudio: typeof (this.hasAudio) === 'undefined' ? true : this.hasAudio,
-        heartTimeout: 5,
-        heartTimeoutReplay: true,
-        heartTimeoutReplayTimes: 3,
-        hiddenAutoPause: false,
-        hotKey: true,
-        isFlv: false,
-        isFullResize: false,
-        isNotMute: this.isNotMute,
-        isResize: true,
-        keepScreenOn: true,
-        loadingText: '请稍等, 视频加载中......',
-        loadingTimeout: 10,
-        loadingTimeoutReplay: true,
-        loadingTimeoutReplayTimes: 3,
-        openWebglAlignment: false,
-        operateBtns: {
-          fullscreen: false,
-          screenshot: false,
-          play: false,
-          audio: false,
-          record: false
-        },
-        recordType: 'mp4',
-        rotate: 0,
-        showBandwidth: false,
-        supportDblclickFullscreen: false,
-        timeout: 10,
-        useMSE: true,
-        useWCS: false,
-        useWebFullScreen: true,
-        videoBuffer: 0.1,
-        wasmDecodeErrorReplay: true,
-        wcsUseVideoRender: true
-      }
-      console.log('Jessibuca -> options: ', options)
-      jessibucaPlayer[this._uid] = new window.Jessibuca({ ...options })
-
-      const jessibuca = jessibucaPlayer[this._uid]
-      const _this = this
-      jessibuca.on('pause', function() {
-        _this.playing = false
-      })
-      jessibuca.on('play', function() {
-        _this.playing = true
-      })
-      jessibuca.on('fullscreen', function(msg) {
-        _this.fullscreen = msg
-      })
-      jessibuca.on('mute', function(msg) {
-        _this.isNotMute = !msg
-      })
-      jessibuca.on('performance', function(performance) {
-        let show = '卡顿'
-        if (performance === 2) {
-          show = '非常流畅'
-        } else if (performance === 1) {
-          show = '流畅'
-        }
-        _this.performance = show
-      })
-      jessibuca.on('kBps', function(kBps) {
-        _this.kBps = Math.round(kBps)
-      })
-      jessibuca.on('videoInfo', function(msg) {
-        console.log('Jessibuca -> videoInfo: ', msg)
-      })
-      jessibuca.on('audioInfo', function(msg) {
-        console.log('Jessibuca -> audioInfo: ', msg)
-      })
-      jessibuca.on('error', function(msg) {
-        console.log('Jessibuca -> error: ', msg)
-      })
-      jessibuca.on('timeout', function(msg) {
-        console.log('Jessibuca -> timeout: ', msg)
-      })
-      jessibuca.on('loadingTimeout', function(msg) {
-        console.log('Jessibuca -> timeout: ', msg)
-      })
-      jessibuca.on('delayTimeout', function(msg) {
-        console.log('Jessibuca -> timeout: ', msg)
-      })
-      jessibuca.on('playToRenderTimes', function(msg) {
-        console.log('Jessibuca -> playToRenderTimes: ', msg)
-      })
-    },
-    playBtnClick: function(event) {
-      this.play(this.videoUrl)
-    },
-    play: function(url) {
-      console.log('Jessibuca -> url: ', url)
-      if (jessibucaPlayer[this._uid]) {
-        this.destroy()
-      }
-      this.create()
-      jessibucaPlayer[this._uid].on('play', () => {
-        this.playing = true
-        this.loaded = true
-        this.quieting = jessibuca.quieting
-      })
-      if (jessibucaPlayer[this._uid].hasLoaded()) {
-        jessibucaPlayer[this._uid].play(url)
-      } else {
-        jessibucaPlayer[this._uid].on('load', () => {
-          jessibucaPlayer[this._uid].play(url)
-        })
-      }
-    },
-    pause: function() {
-      if (jessibucaPlayer[this._uid]) {
-        jessibucaPlayer[this._uid].pause()
-      }
-      this.playing = false
-      this.err = ''
-      this.performance = ''
-    },
-    screenshot: function() {
-      if (jessibucaPlayer[this._uid]) {
-        jessibucaPlayer[this._uid].screenshot()
-      }
-    },
-    mute: function() {
-      if (jessibucaPlayer[this._uid]) {
-        jessibucaPlayer[this._uid].mute()
-      }
-    },
-    cancelMute: function() {
-      if (jessibucaPlayer[this._uid]) {
-        jessibucaPlayer[this._uid].cancelMute()
-      }
-    },
-    destroy: function() {
-      if (jessibucaPlayer[this._uid]) {
-        jessibucaPlayer[this._uid].destroy()
-      }
-      if (document.getElementById('buttonsBox') == null) {
-        this.$refs.container.appendChild(this.btnDom)
-      }
-      jessibucaPlayer[this._uid] = null
-      this.playing = false
-      this.err = ''
-      this.performance = ''
-    },
-    fullscreenSwich: function() {
-      const isFull = this.isFullscreen()
-      
-      if (!isFull) {
-        // 进入全屏
-        try {
-          const container = this.$refs.container;
-          
-          // 尝试使用HTML5全屏API
-          if (container.requestFullscreen) {
-            container.requestFullscreen();
-          } else if (container.webkitRequestFullscreen) {
-            container.webkitRequestFullscreen();
-          } else if (container.msRequestFullscreen) {
-            container.msRequestFullscreen();
-          } else if (container.mozRequestFullScreen) {
-            container.mozRequestFullScreen();
-          } else {
-            // 如果原生API不可用,使用Jessibuca的全屏API
-            if (jessibucaPlayer[this._uid]) {
-              jessibucaPlayer[this._uid].setFullscreen(true);
-            }
-          }
-          
-          // 设置全屏标志
-          this.fullscreen = true;
-        } catch (e) {
-          console.error('全屏切换失败:', e);
-        }
+      // 设置全屏标志
+      fullscreen.value = true
+    } catch (e) {
+      console.error('全屏切换失败:', e)
+    }
+  } else {
+    // 退出全屏
+    try {
+      // #ifdef H5
+      if (document.exitFullscreen) {
+        document.exitFullscreen()
+      } else if (document.webkitExitFullscreen) {
+        document.webkitExitFullscreen()
+      } else if (document.msExitFullscreen) {
+        document.msExitFullscreen()
+      } else if (document.mozCancelFullScreen) {
+        document.mozCancelFullScreen()
       } else {
-        // 退出全屏
-        try {
-          if (document.exitFullscreen) {
-            document.exitFullscreen();
-          } else if (document.webkitExitFullscreen) {
-            document.webkitExitFullscreen();
-          } else if (document.msExitFullscreen) {
-            document.msExitFullscreen();
-          } else if (document.mozCancelFullScreen) {
-            document.mozCancelFullScreen();
-          } else {
-            // 如果原生API不可用,使用Jessibuca的全屏API
-            if (jessibucaPlayer[this._uid]) {
-              jessibucaPlayer[this._uid].setFullscreen(false);
-            }
-          }
-          
-          // 设置全屏标志
-          this.fullscreen = false;
-        } catch (e) {
-          console.error('退出全屏失败:', e);
+        // 如果原生API不可用,使用Jessibuca的全屏API
+        if (jessibucaPlayer[uid]) {
+          jessibucaPlayer[uid].setFullscreen(false)
         }
       }
+      // #endif
       
-      // 重新计算尺寸
-      setTimeout(() => {
-        this.updatePlayerDomSize();
-      }, 300);
+      // 设置全屏标志
+      fullscreen.value = false
+    } catch (e) {
+      console.error('退出全屏失败:', e)
     }
   }
+  
+  // 重新计算尺寸
+  setTimeout(() => {
+    updatePlayerDomSize()
+  }, 300)
 }
+
+// 监听 videoUrl 变化
+watch(() => props.videoUrl, (val) => {
+  nextTick(() => {
+    play(val)
+  })
+}, { immediate: true })
+
+// 组件挂载时
+onMounted(() => {
+  // #ifdef H5
+  const paramUrl = decodeURIComponent(route.params.url || '')
+  nextTick(() => {
+    updatePlayerDomSize()
+    window.onresize = updatePlayerDomSize
+    if (typeof props.videoUrl === 'undefined' && paramUrl) {
+      play(paramUrl)
+    }
+    btnDom.value = document.getElementById('buttonsBox')
+  })
+  // #endif
+})
+
+// 组件卸载前
+onBeforeUnmount(() => {
+  // #ifdef H5
+  if (jessibucaPlayer[uid]) {
+    jessibucaPlayer[uid].destroy()
+  }
+  if (parentNodeResizeObserver.value) {
+    parentNodeResizeObserver.value.disconnect()
+  }
+  // #endif
+  playing.value = false
+  loaded.value = false
+  performance.value = ''
+})
+
+// 暴露方法供父组件调用
+defineExpose({
+  resize,
+  play,
+  pause,
+  destroy,
+  screenshot
+})
+</script>
+<!-- #endif -->
+
+<!-- #ifndef H5 -->
+<script setup>
+// 非 H5 平台不支持 Jessibuca
+console.warn('Jessibuca component is only supported on H5 platform')
 </script>
+<!-- #endif -->
 
 <style>
 .jessibuca-container {

+ 254 - 235
components/common/video-player.vue

@@ -26,254 +26,273 @@
   </view>
 </template>
 
-<script>
-export default {
-  name: 'VideoPlayer',
-  props: {
-    videoUrl: {
-      type: String,
-      required: true
-    },
-    hasAudio: {
-      type: Boolean,
-      default: true
-    },
-    autoplay: {
-      type: Boolean,
-      default: true
-    }
-  },
-  data() {
-    return {
-      isH5: false,
-      isMuted: false,
-      isPlaying: false,
-      isFullscreen: false,
-      jessibucaPlayer: null,
-      livePlayerContext: null,
-      loadError: false
-    }
-  },
-  mounted() {
-    // 判断当前环境
-    // #ifdef H5
-    this.isH5 = true;
-    this.$nextTick(() => {
-      this.initJessibucaPlayer();
-    });
-    // #endif
-    
-    // #ifdef MP-WEIXIN
-    this.isH5 = false;
-    this.initLivePlayer();
-    // #endif
+<script setup>
+import { ref, watch, onMounted, onBeforeUnmount, nextTick, getCurrentInstance } from 'vue'
+
+// Props
+const props = defineProps({
+  videoUrl: {
+    type: String,
+    required: true
   },
-  beforeDestroy() {
-    this.destroyPlayer();
+  hasAudio: {
+    type: Boolean,
+    default: true
   },
-  watch: {
-    videoUrl: {
-      handler(newUrl) {
-        if (this.isH5 && this.jessibucaPlayer) {
-          this.destroyPlayer();
-          this.$nextTick(() => {
-            this.initJessibucaPlayer();
-          });
-        }
-      }
+  autoplay: {
+    type: Boolean,
+    default: true
+  }
+})
+
+// Emits
+const emit = defineEmits(['play', 'pause', 'error', 'fullscreenchange'])
+
+// Template refs
+const jessibucaContainer = ref(null)
+
+// Reactive data
+const isH5 = ref(false)
+const isMuted = ref(false)
+const isPlaying = ref(false)
+const isFullscreen = ref(false)
+const jessibucaPlayer = ref(null)
+const livePlayerContext = ref(null)
+const loadError = ref(false)
+
+// Get current instance for accessing global properties
+const instance = getCurrentInstance()
+
+// 初始化Jessibuca播放器(H5环境)
+const initJessibucaPlayer = () => {
+  if (!isH5.value || !instance?.appContext.config.globalProperties.$jessibuca) {
+    console.error('H5环境或Jessibuca插件未初始化')
+    loadError.value = true
+    return
+  }
+  
+  console.log('正在初始化Jessibuca播放器,URL:', props.videoUrl)
+  
+  // 使用Jessibuca插件
+  instance.appContext.config.globalProperties.$jessibuca.createPlayer({
+    container: jessibucaContainer.value,
+    url: props.videoUrl,
+    hasAudio: props.hasAudio,
+    autoplay: props.autoplay,
+    decoder: './static/js/jessibuca/decoder.js',
+    wasmUrl: './static/js/jessibuca/decoder.wasm'
+  }).then(player => {
+    if (!player) {
+      console.error('Jessibuca播放器创建失败')
+      loadError.value = true
+      return
     }
-  },
-  methods: {
-    // 初始化Jessibuca播放器(H5环境)
-    initJessibucaPlayer() {
-      if (!this.isH5 || !this.$jessibuca) {
-        console.error('H5环境或Jessibuca插件未初始化');
-        this.loadError = true;
-        return;
-      }
-      
-      console.log('正在初始化Jessibuca播放器,URL:', this.videoUrl);
-      
-      // 使用Jessibuca插件
-      this.$jessibuca.createPlayer({
-        container: this.$refs.jessibucaContainer,
-        url: this.videoUrl,
-        hasAudio: this.hasAudio,
-        autoplay: this.autoplay,
-        decoder: './static/js/jessibuca/decoder.js',
-        wasmUrl: './static/js/jessibuca/decoder.wasm'
-      }).then(player => {
-        if (!player) {
-          console.error('Jessibuca播放器创建失败');
-          this.loadError = true;
-          return;
-        }
-        
-        this.jessibucaPlayer = player;
-        console.log('Jessibuca播放器创建成功');
-        
-        // 监听事件
-        player.on('play', () => {
-          this.isPlaying = true;
-          this.loadError = false;
-          this.$emit('play');
-        });
-        
-        player.on('pause', () => {
-          this.isPlaying = false;
-          this.$emit('pause');
-        });
-        
-        player.on('error', (err) => {
-          console.error('Jessibuca播放错误:', err);
-          this.loadError = true;
-          this.$emit('error', err);
-        });
-      }).catch(err => {
-        console.error('Jessibuca播放器初始化失败:', err);
-        this.loadError = true;
-      });
-    },
-    
-    // 初始化小程序live-player
-    initLivePlayer() {
-      if (this.isH5) return;
-      
-      // #ifdef MP-WEIXIN
-      this.livePlayerContext = uni.createLivePlayerContext('videoPlayer', this);
-      if (this.autoplay) {
-        this.play();
-      }
-      // #endif
-    },
     
-    // 播放
-    play() {
-      if (this.isH5 && this.jessibucaPlayer) {
-        this.jessibucaPlayer.play();
-      } else if (this.livePlayerContext) {
-        this.livePlayerContext.play({
-          success: () => {
-            this.isPlaying = true;
-            this.$emit('play');
-          },
-          fail: (err) => {
-            console.error('播放失败:', err);
-            this.$emit('error', err);
-          }
-        });
-      }
-    },
+    jessibucaPlayer.value = player
+    console.log('Jessibuca播放器创建成功')
     
-    // 暂停
-    pause() {
-      if (this.isH5 && this.jessibucaPlayer) {
-        this.jessibucaPlayer.pause();
-      } else if (this.livePlayerContext) {
-        this.livePlayerContext.pause({
-          success: () => {
-            this.isPlaying = false;
-            this.$emit('pause');
-          }
-        });
-      }
-    },
+    // 监听事件
+    player.on('play', () => {
+      isPlaying.value = true
+      loadError.value = false
+      emit('play')
+    })
     
-    // 截图
-    screenshot() {
-      if (this.isH5 && this.jessibucaPlayer) {
-        return this.jessibucaPlayer.screenshot();
-      } else if (this.livePlayerContext) {
-        return new Promise((resolve, reject) => {
-          this.livePlayerContext.snapshot({
-            success: (res) => {
-              resolve(res.tempImagePath);
-            },
-            fail: (err) => {
-              reject(err);
-            }
-          });
-        });
-      }
-    },
+    player.on('pause', () => {
+      isPlaying.value = false
+      emit('pause')
+    })
     
-    // 静音
-    mute() {
-      this.isMuted = true;
-      if (this.isH5 && this.jessibucaPlayer) {
-        this.jessibucaPlayer.mute();
+    player.on('error', (err) => {
+      console.error('Jessibuca播放错误:', err)
+      loadError.value = true
+      emit('error', err)
+    })
+  }).catch(err => {
+    console.error('Jessibuca播放器初始化失败:', err)
+    loadError.value = true
+  })
+}
+
+// 初始化小程序live-player
+const initLivePlayer = () => {
+  if (isH5.value) return
+  
+  // #ifdef MP-WEIXIN
+  livePlayerContext.value = uni.createLivePlayerContext('videoPlayer', instance)
+  if (props.autoplay) {
+    play()
+  }
+  // #endif
+}
+
+// 播放
+const play = () => {
+  if (isH5.value && jessibucaPlayer.value) {
+    jessibucaPlayer.value.play()
+  } else if (livePlayerContext.value) {
+    livePlayerContext.value.play({
+      success: () => {
+        isPlaying.value = true
+        emit('play')
+      },
+      fail: (err) => {
+        console.error('播放失败:', err)
+        emit('error', err)
       }
-    },
-    
-    // 取消静音
-    cancelMute() {
-      this.isMuted = false;
-      if (this.isH5 && this.jessibucaPlayer) {
-        this.jessibucaPlayer.cancelMute();
+    })
+  }
+}
+
+// 暂停
+const pause = () => {
+  if (isH5.value && jessibucaPlayer.value) {
+    jessibucaPlayer.value.pause()
+  } else if (livePlayerContext.value) {
+    livePlayerContext.value.pause({
+      success: () => {
+        isPlaying.value = false
+        emit('pause')
       }
-    },
-    
-    // 全屏
-    fullscreenSwich() {
-      if (this.isH5 && this.jessibucaPlayer) {
-        this.jessibucaPlayer.fullscreen();
-      } else if (this.livePlayerContext) {
-        if (this.isFullscreen) {
-          this.livePlayerContext.exitFullScreen();
-        } else {
-          this.livePlayerContext.requestFullScreen({
-            direction: 90
-          });
+    })
+  }
+}
+
+// 截图
+const screenshot = () => {
+  if (isH5.value && jessibucaPlayer.value) {
+    return jessibucaPlayer.value.screenshot()
+  } else if (livePlayerContext.value) {
+    return new Promise((resolve, reject) => {
+      livePlayerContext.value.snapshot({
+        success: (res) => {
+          resolve(res.tempImagePath)
+        },
+        fail: (err) => {
+          reject(err)
         }
-      }
-    },
-    
-    // 重置大小
-    resize() {
-      if (this.isH5 && this.jessibucaPlayer) {
-        this.jessibucaPlayer.resize();
-      }
-    },
-    
-    // 检查是否全屏
-    isFullscreen() {
-      return this.isFullscreen;
-    },
-    
-    // live-player状态变化
-    onStateChange(e) {
-      console.log('播放器状态变化:', e.detail);
-      const state = e.detail.code;
-      if (state === 2003) { // 播放中
-        this.isPlaying = true;
-        this.$emit('play');
-      } else if (state === 2004) { // 暂停
-        this.isPlaying = false;
-        this.$emit('pause');
-      }
-    },
-    
-    // live-player错误
-    onError(e) {
-      console.error('播放器错误:', e.detail);
-      this.$emit('error', e.detail);
-    },
-    
-    // 全屏状态变化
-    onFullscreenChange(e) {
-      this.isFullscreen = e.detail.fullScreen;
-      this.$emit('fullscreenchange', this.isFullscreen);
-    },
-    
-    // 销毁播放器
-    destroyPlayer() {
-      if (this.isH5 && this.jessibucaPlayer) {
-        this.jessibucaPlayer.destroy();
-        this.jessibucaPlayer = null;
-      }
+      })
+    })
+  }
+}
+
+// 静音
+const mute = () => {
+  isMuted.value = true
+  if (isH5.value && jessibucaPlayer.value) {
+    jessibucaPlayer.value.mute()
+  }
+}
+
+// 取消静音
+const cancelMute = () => {
+  isMuted.value = false
+  if (isH5.value && jessibucaPlayer.value) {
+    jessibucaPlayer.value.cancelMute()
+  }
+}
+
+// 全屏
+const fullscreenSwich = () => {
+  if (isH5.value && jessibucaPlayer.value) {
+    jessibucaPlayer.value.fullscreen()
+  } else if (livePlayerContext.value) {
+    if (isFullscreen.value) {
+      livePlayerContext.value.exitFullScreen()
+    } else {
+      livePlayerContext.value.requestFullScreen({
+        direction: 90
+      })
     }
   }
 }
+
+// 重置大小
+const resize = () => {
+  if (isH5.value && jessibucaPlayer.value) {
+    jessibucaPlayer.value.resize()
+  }
+}
+
+// 检查是否全屏
+const isFullscreenCheck = () => {
+  return isFullscreen.value
+}
+
+// live-player状态变化
+const onStateChange = (e) => {
+  console.log('播放器状态变化:', e.detail)
+  const state = e.detail.code
+  if (state === 2003) { // 播放中
+    isPlaying.value = true
+    emit('play')
+  } else if (state === 2004) { // 暂停
+    isPlaying.value = false
+    emit('pause')
+  }
+}
+
+// live-player错误
+const onError = (e) => {
+  console.error('播放器错误:', e.detail)
+  emit('error', e.detail)
+}
+
+// 全屏状态变化
+const onFullscreenChange = (e) => {
+  isFullscreen.value = e.detail.fullScreen
+  emit('fullscreenchange', isFullscreen.value)
+}
+
+// 销毁播放器
+const destroyPlayer = () => {
+  if (isH5.value && jessibucaPlayer.value) {
+    jessibucaPlayer.value.destroy()
+    jessibucaPlayer.value = null
+  }
+}
+
+// Watch videoUrl changes
+watch(() => props.videoUrl, () => {
+  if (isH5.value && jessibucaPlayer.value) {
+    destroyPlayer()
+    nextTick(() => {
+      initJessibucaPlayer()
+    })
+  }
+})
+
+// Lifecycle hooks
+onMounted(() => {
+  // 判断当前环境
+  // #ifdef H5
+  isH5.value = true
+  nextTick(() => {
+    initJessibucaPlayer()
+  })
+  // #endif
+  
+  // #ifdef MP-WEIXIN
+  isH5.value = false
+  initLivePlayer()
+  // #endif
+})
+
+onBeforeUnmount(() => {
+  destroyPlayer()
+})
+
+// Expose methods for parent component access
+defineExpose({
+  play,
+  pause,
+  screenshot,
+  mute,
+  cancelMute,
+  fullscreenSwich,
+  resize,
+  isFullscreen: isFullscreenCheck
+})
 </script>
 
 <style>

+ 31 - 16
config/api.js

@@ -1,26 +1,41 @@
-// 开发环境
+/**
+ * API 配置
+ * 
+ * 环境说明:
+ * 1. H5 开发环境:使用 /base 代理路径,通过 vite proxy 转发到本地服务器
+ * 2. H5 生产环境:使用真实域名
+ * 3. 小程序/App/鸿蒙:始终使用真实域名(不支持 proxy)
+ */
+
+// 判断是否为 H5 平台
+// #ifdef H5
+const isH5 = true;
+// #endif
+// #ifndef H5
+const isH5 = false;
+// #endif
+
+// 开发环境配置
 const dev = {
-  serve: "http://localhost:8080",
-  upload: "http://nxy.gbdfarm.com"
+  // H5 开发环境使用代理路径,其他平台使用真实域名
+  serve: isH5 ? "/base" : "https://nxy.gbdfarm.com:9000/pro-uniapp",
+  upload: import.meta.env.VITE_UPLOAD_URL || "http://nxy.gbdfarm.com"
 };
-// 生产环境
+
+// 生产环境配置
 const prod = {
-  serve: "https://nxy.gbdfarm.com:9000/pro-uniapp",
-  upload: "https://nxy.gbdfarm.com"
+  serve: import.meta.env.VITE_BASE_URL || "https://nxy.gbdfarm.com:9000/pro-uniapp",
+  upload: import.meta.env.VITE_UPLOAD_URL || "https://nxy.gbdfarm.com"
 };
 
-//默认生产环境
-let api = dev;
-//如果是开发环境
-if (process.env.NODE_ENV == "development") {
-  api = dev;
-} else {
-  api = prod;
-}
-//微信小程序,app的打包方式建议为生产环境,所以这块直接条件编译赋值
-// #ifdef MP-WEIXIN || APP-PLUS
+// 根据环境选择配置
+let api = process.env.NODE_ENV === "development" ? dev : prod;
+
+// 微信小程序、App、鸿蒙始终使用生产环境配置
+// #ifdef MP-WEIXIN || APP-PLUS || MP-HARMONY
 api = prod;
 // #endif
+
 export default {
   ...api,
 };

+ 1 - 0
index.html

@@ -3,6 +3,7 @@
   <head>
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
+    <link rel="icon" href="/static/logo.png" type="image/png" />
     <title>农小禹</title>
   </head>
   <body>

+ 20 - 0
jsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./*"]
+    },
+    "types": ["@dcloudio/types"],
+    "typeRoots": ["./types", "./node_modules/@types"]
+  },
+  "include": [
+    "**/*.js",
+    "**/*.vue",
+    "types/**/*.d.ts"
+  ],
+  "exclude": [
+    "node_modules",
+    "unpackage",
+    "dist"
+  ]
+}

+ 23 - 38
main.js

@@ -1,41 +1,26 @@
-// main.js(Vue 2)
-import Vue from 'vue'
-import App from './App'
+// main.js(Vue 3)
+import { createSSRApp } from 'vue'
+import App from './App.vue'
 import * as filters from './utils/filters.js'
 import store from './store'
-import uView from 'uview-ui'
-import config from '@/config/config'
-import JessibucaPlugin from './utils/jessibuca-plugin'
-
-// 注册全局过滤器
-Object.keys(filters).forEach((key) => {
-  Vue.filter(key, filters[key])
-})
-
-// 全局挂载变量(通过 Vue.prototype)
-// Vue.prototype.$mainColor = config.mainColor
-// Vue.prototype.$lightColor = config.lightColor
-// Vue.prototype.$aiderLightColor = config.aiderLightColor
-
-// 使用 Vuex store
-Vue.use(store)
-
-// 使用 uView UI
-Vue.use(uView)
-
-// 动态加载 Jessibuca 插件,仅限 H5
-// #ifdef H5
-if (typeof window !== 'undefined') {
-  Vue.use(JessibucaPlugin)
+import uviewPlus from 'uview-plus'
+
+export function createApp() {
+  const app = createSSRApp(App)
+  
+  // 使用 uview-plus
+  app.use(uviewPlus)
+  
+  // 使用 Vuex store
+  app.use(store)
+  
+  // 迁移全局过滤器为全局方法
+  // Vue3 移除了过滤器功能,改为全局方法
+  Object.keys(filters).forEach((key) => {
+    app.config.globalProperties['$' + key] = filters[key]
+  })
+  
+  return {
+    app
+  }
 }
-// #endif
-
-Vue.config.productionTip = false
-
-App.mpType = 'app'
-
-const app = new Vue({
-  store,
-  ...App
-})
-app.$mount() // 必须是这样写,不要在这里指定挂载节点

+ 27 - 0
manifest.json

@@ -87,6 +87,7 @@
     },
     "mp-weixin" : {
         "appid" : "wxc738cddfb96a9176",
+        "mergeVirtualHostAttributes" : true,
         "setting" : {
             "urlCheck" : false
         },
@@ -122,5 +123,31 @@
         "unipush" : {
             "enable" : false
         }
+    },
+    "app-harmony" : {
+        "distribute" : {
+            "bundleName" : "gbd.nongxiaoyu.release",
+            "signingConfigs" : {
+                "default" : {
+                    "certpath" : "c:\\Users\\郭佳乐\\AppData\\Roaming\\HBuilder X\\extensions\\launcher\\agc-certs\\1769496701688.cer",
+                    "keyAlias" : "debugKey",
+                    "keyPassword" : "0000001B93A9263BB789139CB4E3716C5A6734683DB1B15B40ACF8925020C5A49A842FEAB86A0EF46F6C59",
+                    "profile" : "c:\\Users\\郭佳乐\\AppData\\Roaming\\HBuilder X\\extensions\\launcher\\agc-certs\\1769496701688.p7b",
+                    "signAlg" : "SHA256withECDSA",
+                    "storeFile" : "c:\\Users\\郭佳乐\\AppData\\Roaming\\HBuilder X\\extensions\\launcher\\agc-certs\\1769496701688.p12",
+                    "storePassword" : "0000001B93A9263BB789139CB4E3716C5A6734683DB1B15B40ACF8925020C5A49A842FEAB86A0EF46F6C59"
+                },
+                "release" : {
+                    "certpath" : "E:/company/nongye/file/harmony-configs/nongxiaoyu_release.cer",
+                    "keyAlias" : "nongxiaoyu-release",
+                    "keyPassword" : "000000182BA718191A7DB7EB1AB9F604017FAEF415C2C68DCDC6A9FDE6B64D7FF853EC8879E390F5",
+                    "profile" : "E:/company/nongye/file/harmony-configs/nongxiaoyu_releaseRelease.p7b",
+                    "signAlg" : "SHA256withECDSA",
+                    "storeFile" : "E:/company/nongye/file/harmony-configs/nongxiaoyu_release.p12",
+                    "storePassword" : "000000183CA17B5B84A9254AE22110A870028282312AD3CE750BD9E8FB4B54AEF55968977862EEF9"
+                }
+            },
+            "modules" : {}
+        }
     }
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4506 - 1
package-lock.json


+ 38 - 26
package.json

@@ -4,33 +4,45 @@
 	"description": "农小禹智慧农业系统",
 	"main": "main.js",
 	"scripts": {
-	  "dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve",
-	  "dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
-	  "build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
-	  "build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build"
+		"dev:h5": "cross-env UNI_INPUT_DIR=./ uni -p h5",
+		"dev:mp-weixin": "cross-env UNI_INPUT_DIR=./ uni -p mp-weixin",
+		"build:h5": "cross-env UNI_INPUT_DIR=./ uni build -p h5",
+		"build:mp-weixin": "cross-env UNI_INPUT_DIR=./ uni build -p mp-weixin",
+		"test:deps": "node tests/dependency-verification.test.js",
+		"test:main": "node tests/main-migration-verification.test.js",
+		"test:store": "node tests/store-functionality.test.js",
+		"test:utils": "node tests/utils-functions.test.js",
+		"test:dict-tag": "node tests/dict-tag-component-property.test.js",
+		"test:dict-tag-unit": "node tests/dict-tag-component-unit.test.js",
+		"test:location-picker": "node tests/location-picker-component-property.test.js",
+		"test:location-picker-functional": "node tests/location-picker-component-functional.test.js",
+		"test:jessibuca": "node tests/jessibuca-component-functional.test.js",
+		"test:login-store": "node tests/login-page-store-property.test.js",
+		"test:comprehensive": "node tests/comprehensive-property-tests.test.js",
+		"test:cross-platform": "node tests/cross-platform-functional.test.js"
 	},
-	"keywords": ["农业", "物联网", "智慧农业"],
+	"keywords": [
+		"农业",
+		"物联网",
+		"智慧农业"
+	],
 	"author": "",
 	"license": "ISC",
-  "dependencies": {
-    "event-source-polyfill": "^1.0.31",
-    "uview-plus": "^3.2.0",
-    "@dcloudio/uni-app": "^3.0.0-alpha-4020920240930001",
-    "@dcloudio/uni-h5": "^3.0.0-alpha-4020920240930001",
-    "@dcloudio/uni-mp-weixin": "^3.0.0-alpha-4020920240930001",
-    "@dcloudio/uni-mp-harmony": "^3.0.0-alpha-4020920240930001",
-    "vue": "^3.4.21",
-    "vuex": "^4.1.0"
-  },
-  "devDependencies": {
-    "@dcloudio/uni-cli-shared": "^3.0.0-alpha-4020920240930001",
-    "@dcloudio/vue-cli-plugin-uni": "^3.0.0-alpha-4020920240930001",
-    "@dcloudio/vite-plugin-uni": "^3.0.0-alpha-4020920240930001",
-    "@vue/cli-service": "^5.0.8",
-    "cross-env": "^7.0.3",
-    "sass": "^1.69.5",
-    "sass-loader": "^13.3.2",
-    "vite": "^5.0.0",
-    "@vitejs/plugin-vue": "^5.0.0"
-  }
+	"dependencies": {
+		"@dcloudio/uni-app": "3.0.0-4020920240930001",
+		"@dcloudio/uni-h5": "3.0.0-4020920240930001",
+		"@dcloudio/uni-mp-weixin": "3.0.0-4020920240930001",
+		"@vitejs/plugin-vue": "^5.2.1",
+		"event-source-polyfill": "^1.0.31",
+		"uview-plus": "^3.2.0",
+		"vue": "^3.4.21",
+		"vuex": "^4.1.0"
+	},
+	"devDependencies": {
+		"@dcloudio/uni-cli-shared": "3.0.0-4020920240930001",
+		"@dcloudio/vite-plugin-uni": "3.0.0-4020920240930001",
+		"cross-env": "^10.1.0",
+		"sass": "^1.69.5",
+		"vite": "^5.2.8"
+	}
 }

+ 8 - 3
pages.json

@@ -348,7 +348,12 @@
 		]
 	},
 	"easycom": {
-		"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
-		"^dict-tag$": "components/common/dict-tag.vue"
-	}
+		"autoscan": true,
+		"custom": {
+			"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
+			"^u-(.*)": "uview-plus/components/u-$1/u-$1.vue",
+			"^dict-tag$": "components/common/dict-tag.vue"
+		}
+	},
+	"vueVersion": "3"
 }

+ 28 - 33
pages/about/index.vue

@@ -48,39 +48,34 @@
   </view>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      version: '1.0.0'
-    };
-  },
-  
-  methods: {
-    // 复制文本
-    copyText(text) {
-      uni.setClipboardData({
-        data: text,
-        success: () => {
-          uni.showToast({
-            title: '已复制到剪贴板',
-            icon: 'none'
-          });
-        }
-      });
-    },
-
-    // 拨打电话
-    makePhoneCall() {
-      uni.makePhoneCall({
-        phoneNumber: '400-xxx-xxxx',
-        fail: () => {
-          this.copyText('400-xxx-xxxx');
-        }
-      });
-    }
-  }
-};
+<script setup>
+import { ref } from 'vue'
+
+// 响应式数据
+const version = ref('1.0.0')
+
+// 复制文本
+const copyText = (text) => {
+	uni.setClipboardData({
+		data: text,
+		success: () => {
+			uni.showToast({
+				title: '已复制到剪贴板',
+				icon: 'none'
+			})
+		}
+	})
+}
+
+// 拨打电话
+const makePhoneCall = () => {
+	uni.makePhoneCall({
+		phoneNumber: '400-xxx-xxxx',
+		fail: () => {
+			copyText('400-xxx-xxxx')
+		}
+	})
+}
 </script>
 
 <style lang="scss">

+ 853 - 0
pages/activity/activity-detail-new.vue

@@ -0,0 +1,853 @@
+<template>
+  <view class="page-container">
+    <!-- 页面滚动区域 -->
+    <scroll-view class="page-scroll" scroll-y>
+      <!-- 地块信息卡片 -->
+      <view class="info-card">
+        <view class="card-title">
+          <text>地块信息</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">地块名称</text>
+          <text class="info-value">{{ formData.fieldName || '未知' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">作物名称</text>
+          <text class="info-value">{{ formData.growCrops || '未知' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">负责人</text>
+          <text class="info-value">{{ formData.manager || '未知' }}</text>
+        </view>
+      </view>
+
+      <!-- 任务填写表单 -->
+      <view class="form-card">
+        <view class="card-title">
+          <text>任务信息</text>
+        </view>
+        
+        <!-- 任务名称 -->
+        <view class="form-item">
+          <view class="form-label required">任务名称</view>
+          <input 
+            v-model="formData.taskName"
+            placeholder="请输入任务名称,例如:水稻田施肥"
+            :disabled="pageMode === 'view'"
+            class="form-input"
+          />
+        </view>
+
+        <!-- 任务类型 -->
+        <view class="form-item" @click="pageMode !== 'view' && showTaskTypeSelector()">
+          <view class="form-label required">任务类型</view>
+          <view class="select-wrapper">
+<!--            <input 
+              :value="formData.typeName"
+              :placeholder="dictLoading ? '加载中...' : '请选择任务类型'"
+			  readonly
+              class="form-input select-input"
+            /> -->
+			<view class="form-input select-input">
+			      {{ formData.typeName || (dictLoading ? '加载中...' : '请选择任务类型') }}
+			</view>
+            <view v-if="pageMode !== 'view'" class="select-arrow">
+              <text>▼</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 执行时间 -->
+        <view class="form-item" @click="pageMode !== 'view' && selectExecuteTime()">
+          <view class="form-label required">执行时间</view>
+          <view class="select-wrapper">
+<!--            <input 
+              :value="formattedExecuteTime"
+              placeholder="请选择任务计划时间"
+              readonly
+              class="form-input select-input"
+              readonly
+            /> -->
+			<view class="form-input select-input">
+			      {{ formattedExecuteTime || '请选择任务计划时间' }}
+			</view>
+            <view v-if="pageMode !== 'view'" class="select-arrow">
+              <text>选择</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 负责人选择 -->
+        <view class="form-item" @click="pageMode !== 'view' && showUserSelector()">
+          <view class="form-label required">负责人</view>
+          <view class="select-wrapper">
+<!--            <input 
+              :value="formData.assigneeName"
+              placeholder="请选择负责人"
+			  readonly
+              class="form-input select-input"
+            /> -->
+			<view class="form-input select-input">
+			      {{ formData.assigneeName || '请选择负责人' }}
+			</view>
+            <view v-if="pageMode !== 'view'" class="select-arrow">
+              <text>选择</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 任务说明 -->
+        <view class="form-item">
+          <view class="form-label">任务说明</view>
+          <textarea 
+            v-model="formData.remark"
+            placeholder="请输入任务要点,例如:每亩用肥20kg"
+            :disabled="pageMode === 'view'"
+            class="form-textarea"
+            maxlength="200"
+          ></textarea>
+          <view class="char-count">{{ (formData.remark || '').length }}/200</view>
+        </view>
+
+        <!-- 任务完成情况 -->
+        <view class="section-divider"></view>
+        <view class="section-title">
+          <text>任务完成情况</text>
+        </view>
+
+        <!-- 完成状态选择 - 新建和编辑模式 -->
+        <view class="form-item" v-if="pageMode === 'create' || pageMode === 'edit'">
+          <view class="form-label">完成状态</view>
+          <view class="radio-group">
+            <view 
+              class="radio-item" 
+              v-for="(item, index) in completionStatusOptions" 
+              :key="index"
+              @click="selectCompletionStatus(item.value)"
+            >
+              <view class="radio-circle" :class="{'radio-checked': formData.completionStatus === item.value}">
+                <view v-if="formData.completionStatus === item.value" class="radio-dot"></view>
+              </view>
+              <text class="radio-label">{{ item.label }}</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 查看模式显示完成状态 -->
+        <view class="form-item" v-if="pageMode === 'view'">
+          <view class="form-label">完成状态</view>
+          <view class="status-completed">
+            <text class="status-icon">✓</text>
+            <text>已完成</text>
+          </view>
+        </view>
+
+        <!-- 完成时间 -->
+        <view class="form-item" v-if="formData.completionStatus === '1'">
+          <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成时间</view>
+                     <view class="select-wrapper" @click="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1' && selectCompletionTime()">
+            <!-- <input 
+              :value="formattedCompletionTime"
+              placeholder="请选择实际完成时间"
+              readonly
+              class="form-input select-input"
+            /> -->
+			<view class="form-input select-input">
+			      {{ formattedCompletionTime || '请选择实际完成时间' }}
+			</view>
+                <view v-if="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'" class="select-arrow">
+               <text>选择</text>
+             </view>
+          </view>
+        </view>
+
+        <!-- 完成说明 -->
+        <view class="form-item" v-if="formData.completionStatus === '1'">
+          <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成说明</view>
+          <textarea 
+            v-model="formData.completionDesc"
+            placeholder="请输入完成说明,例如:已完成并拍照记录"
+            :disabled="pageMode === 'view'"
+            class="form-textarea"
+            maxlength="300"
+          ></textarea>
+          <view class="char-count">{{ (formData.completionDesc || '').length }}/300</view>
+          <view class="form-error" v-if="formErrors.completionDesc">
+            {{ formErrors.completionDesc }}
+          </view>
+        </view>
+
+        <!-- 现场图片 -->
+        <view class="form-item" v-if="formData.completionStatus === '1'">
+          <view class="form-label">现场图片</view>
+          
+          <!-- 新建和编辑模式 -->
+          <view v-if="pageMode === 'create' || pageMode === 'edit'" class="image-upload">
+            <view class="image-list">
+              <view 
+                class="image-preview" 
+                v-for="(item, index) in formData.images" 
+                :key="index"
+                @click="previewImage(item, index)"
+              >
+                <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
+                <image :src="item.url" mode="aspectFill"/>
+                <view class="delete-btn" @click.stop="deletePic(index)">
+                  <text>×</text>
+                </view>
+              </view>
+              <view 
+                v-if="formData.images.length < 6" 
+                class="upload-btn" 
+                @click="chooseImage"
+              >
+                <text class="upload-icon">+</text>
+                <text class="upload-text">添加图片</text>
+              </view>
+            </view>
+            <view class="upload-tip">最多可上传6张图片</view>
+          </view>
+
+          <!-- 查看模式 -->
+          <view v-else-if="pageMode === 'view' && formData.images && formData.images.length > 0" class="image-view">
+            <view class="image-list">
+              <view 
+                class="image-preview" 
+                v-for="(item, index) in formData.images" 
+                :key="index"
+                @click="previewImage(item, index)"
+              >
+                <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
+                <image :src="item.url" mode="aspectFill"/>
+              </view>
+            </view>
+          </view>
+
+          <!-- 无图片提示 -->
+          <view v-else class="no-images">
+            <text>暂无图片</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 底部占位 -->
+      <view class="bottom-safe"></view>
+    </scroll-view>
+
+    <!-- 底部提交按钮 -->
+    <view class="footer-safe" v-if="pageMode !== 'view'">
+      <view class="footer-content">
+        <button 
+          class="submit-button"
+          :class="{'loading': isSubmitting}"
+          @click="submitForm"
+          :disabled="isSubmitting"
+        >
+          {{ isSubmitting ? '提交中...' : submitButtonText }}
+        </button>
+      </view>
+    </view>
+
+
+
+    <!-- 遮罩层 -->
+    <view v-if="showTaskTypePicker || showDateTimeSelector || showUserPicker" class="picker-mask" @click="closePickers"></view>
+    
+    <!-- 任务类型选择弹窗 -->
+    <view v-if="showTaskTypePicker" class="picker-popup">
+      <view class="picker-header">
+        <text class="picker-cancel" @click="showTaskTypePicker = false">取消</text>
+        <text class="picker-title">选择任务类型</text>
+        <text class="picker-confirm" @click="confirmTaskType">确定</text>
+      </view>
+      <view class="picker-content">
+        <view v-if="dictLoading" class="picker-loading">
+          <text>加载中...</text>
+        </view>
+        <view 
+          v-else
+          class="picker-item" 
+          v-for="(item, index) in taskTypeOptions" 
+          :key="index"
+          :class="{'selected': tempTaskTypeIndex === index}"
+          @click="tempTaskTypeIndex = index"
+        >
+          <text>{{ item.dictLabel }}</text>
+          <text v-if="tempTaskTypeIndex === index" class="check-mark">✓</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 用户选择弹窗 -->
+    <view v-if="showUserPicker" class="picker-popup">
+      <view class="picker-header">
+        <text class="picker-cancel" @click="showUserPicker = false">取消</text>
+        <text class="picker-title">选择负责人</text>
+        <text class="picker-confirm" @click="confirmUser">确定</text>
+      </view>
+      <view class="picker-content">
+        <view v-if="usersLoading" class="picker-loading">
+          <text>加载中...</text>
+        </view>
+        <view v-else-if="userList.length === 0" class="picker-empty">
+          <text>暂无可选负责人</text>
+        </view>
+        <view 
+          v-else
+          class="picker-item" 
+          v-for="(user, index) in userList" 
+          :key="user.userId"
+          :class="{'selected': tempUserIndex === index}"
+          @click="tempUserIndex = index"
+        >
+          <text>{{ user.userName }}</text>
+          <text v-if="tempUserIndex === index" class="check-mark">✓</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 日期时间选择弹窗 -->
+    <view v-if="showDateTimeSelector" class="picker-popup datetime-picker">
+      <view class="picker-header">
+        <text class="picker-cancel" @click="cancelDateTime">取消</text>
+        <text class="picker-title">选择时间</text>
+        <text class="picker-confirm" @click="confirmDateTime">确定</text>
+      </view>
+      <view class="datetime-picker-content">
+        <picker-view 
+          class="datetime-picker-view"
+          :value="dateTimePickerValue" 
+          @change="onDateTimePickerChange"
+          :indicator-style="'height: 80rpx;'"
+          :mask-style="'background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6)), linear-gradient(0deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));'"
+        >
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[0]" :key="'year-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[1]" :key="'month-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[2]" :key="'day-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[3]" :key="'hour-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[4]" :key="'min-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+        </picker-view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import api from "@/config/api.js";
+import { getAgriculturalTasksById, addAgriculturalTask, updateAgriculturalTask } from '@/api/services/activity.js';
+import { getUsersByPlotId, getUserInfo } from '@/api/services/user.js';
+import { useDict } from '@/utils/composables/useDict';
+import storage from "@/utils/storage.js";
+
+// 使用字典组合式函数
+const { dictData, dictLoading, loadDict } = useDict(['task_type', 'task_status'])
+
+// 响应式数据
+
+</script>
+
+<style scoped>
+.page-container {
+  height: 100vh;
+  background-color: #F5F5F5;
+  display: flex;
+  flex-direction: column;
+}
+
+.page-scroll {
+  flex: 1;
+  overflow: hidden;
+}
+
+/* 地块基础信息卡片 */
+.info-card {
+  background: #F9F9F9;
+  margin: 24rpx;
+  padding: 24rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid #E5E5E5;
+}
+
+.card-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333333;
+  margin-bottom: 16rpx;
+  padding-bottom: 12rpx;
+  border-bottom: 1rpx solid #E5E5E5;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+  padding: 8rpx 0;
+}
+
+.info-item:last-child {
+  margin-bottom: 0;
+}
+
+.info-label {
+  font-size: 28rpx;
+  color: #666666;
+  flex-shrink: 0;
+}
+
+.info-value {
+  font-size: 28rpx;
+  color: #333333;
+  font-weight: 500;
+}
+
+/* 任务填写表单 */
+.form-card {
+  background: #FFFFFF;
+  margin: 0 24rpx 24rpx;
+  padding: 24rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid #E5E5E5;
+}
+
+.section-divider {
+  height: 1rpx;
+  background: #F0F0F0;
+  margin: 32rpx 0 24rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333333;
+  margin-bottom: 16rpx;
+  padding-bottom: 12rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+
+.form-item {
+  margin-bottom: 24rpx;
+}
+
+.form-item:last-child {
+  margin-bottom: 0;
+}
+
+.form-label {
+  font-size: 28rpx;
+  color: #333333;
+  margin-bottom: 12rpx;
+  font-weight: 500;
+}
+
+.form-label.required::before {
+  content: '*';
+  color: #FF6B6B;
+  margin-right: 4rpx;
+}
+
+.form-input {
+  width: 100%;
+  line-height: 80rpx;
+  height: 80rpx;
+  background: #F8F8F8;
+  border: 1rpx solid #E5E5E5;
+  border-radius: 8rpx;
+  padding: 0 16rpx;
+  font-size: 28rpx;
+  color: #333333;
+  box-sizing: border-box;
+}
+
+.form-input:focus {
+  background: #FFFFFF;
+  border-color: #3BB44A;
+}
+
+.form-input::placeholder {
+  color: #CCCCCC;
+}
+
+.form-input:disabled {
+  background: #F8F8F8;
+  color: #999999;
+}
+
+.select-wrapper {
+  position: relative;
+  width: 100%;
+}
+
+.select-input {
+  cursor: pointer;
+}
+
+.select-arrow {
+  position: absolute;
+  right: 16rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #999999;
+  font-size: 20rpx;
+  pointer-events: none;
+}
+
+.form-textarea {
+  width: 100%;
+  min-height: 120rpx;
+  background: #F8F8F8;
+  border: 1rpx solid #E5E5E5;
+  border-radius: 8rpx;
+  padding: 16rpx;
+  font-size: 28rpx;
+  color: #333333;
+  box-sizing: border-box;
+  resize: none;
+  line-height: 1.5;
+}
+
+.form-textarea:focus {
+  background: #FFFFFF;
+  border-color: #3BB44A;
+}
+
+.form-textarea::placeholder {
+  color: #CCCCCC;
+}
+
+.form-textarea:disabled {
+  background: #F8F8F8;
+  color: #999999;
+}
+
+.char-count {
+  font-size: 24rpx;
+  color: #CCCCCC;
+  text-align: right;
+  margin-top: 8rpx;
+}
+
+.form-error {
+  font-size: 24rpx;
+  color: #FF6B6B;
+  margin-top: 8rpx;
+}
+
+/* 单选按钮样式 */
+.radio-group {
+  display: flex;
+  gap: 32rpx;
+}
+
+.radio-item {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+}
+
+.radio-circle {
+  width: 32rpx;
+  height: 32rpx;
+  border: 2rpx solid #CCCCCC;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 8rpx;
+}
+
+.radio-circle.radio-checked {
+  border-color: #3BB44A;
+  background-color: #3BB44A;
+}
+
+.radio-dot {
+  width: 12rpx;
+  height: 12rpx;
+  background-color: #FFFFFF;
+  border-radius: 50%;
+}
+
+.radio-label {
+  font-size: 28rpx;
+  color: #333333;
+}
+
+/* 完成状态样式 */
+.status-completed {
+  display: flex;
+  align-items: center;
+  color: #3BB44A;
+  font-size: 28rpx;
+}
+
+.status-icon {
+  width: 32rpx;
+  height: 32rpx;
+  background: #3BB44A;
+  color: #FFFFFF;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 8rpx;
+  font-size: 20rpx;
+  line-height: 1;
+}
+
+/* 图片上传样式 */
+.image-upload, .image-view {
+  width: 100%;
+}
+
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.image-preview {
+  position: relative;
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 8rpx;
+  overflow: hidden;
+  background: #F5F5F5;
+}
+
+.image-preview image {
+  width: 100%;
+  height: 100%;
+}
+
+.delete-btn {
+  position: absolute;
+  top: -6rpx;
+  right: -6rpx;
+  width: 32rpx;
+  height: 32rpx;
+  background: rgba(0, 0, 0, 0.6);
+  color: #FFFFFF;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20rpx;
+  line-height: 1;
+}
+
+.upload-btn {
+  width: 160rpx;
+  height: 160rpx;
+  background: #F8F8F8;
+  border: 2rpx dashed #DDDDDD;
+  border-radius: 8rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.upload-icon {
+  font-size: 32rpx;
+  margin-bottom: 8rpx;
+  color: #999999;
+}
+
+.upload-text {
+  font-size: 24rpx;
+  color: #999999;
+}
+
+.upload-tip {
+  font-size: 24rpx;
+  color: #999999;
+  margin-top: 12rpx;
+}
+
+.no-images {
+  padding: 40rpx 0;
+  text-align: center;
+  color: #CCCCCC;
+  font-size: 28rpx;
+}
+
+/* 底部安全区域 */
+.bottom-safe {
+  height: 120rpx;
+}
+
+.footer-safe {
+  background: #FFFFFF;
+  border-top: 1rpx solid #E5E5E5;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.footer-content {
+  padding: 24rpx;
+}
+
+.submit-button {
+  width: 100%;
+  height: 88rpx;
+  background: #3BB44A;
+  color: #FFFFFF;
+  border: none;
+  border-radius: 8rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.submit-button:active {
+  background: #2D8C3C;
+}
+
+.submit-button:disabled,
+.submit-button.loading {
+  background: #CCCCCC;
+}
+
+/* 选择器弹窗样式 */
+.picker-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 999;
+}
+
+.picker-popup {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  background: #FFFFFF;
+  border-radius: 16rpx 16rpx 0 0;
+  z-index: 1000;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.picker-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx;
+  border-bottom: 1rpx solid #E5E5E5;
+}
+
+.picker-cancel, .picker-confirm {
+  font-size: 30rpx;
+  color: #3BB44A;
+}
+
+.picker-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333333;
+}
+
+.picker-content {
+  max-height: 400rpx;
+  overflow-y: auto;
+}
+
+.picker-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  font-size: 30rpx;
+  color: #333333;
+}
+
+.picker-item.selected {
+  color: #3BB44A;
+}
+
+.picker-item:last-child {
+  border-bottom: none;
+}
+
+.check-mark {
+  color: #3BB44A;
+  font-size: 28rpx;
+}
+
+/* 日期时间选择器样式 */
+.datetime-picker {
+  height: 60vh;
+}
+
+.datetime-picker-content {
+  height: calc(100% - 100rpx);
+  padding: 0;
+}
+
+.datetime-picker-view {
+  width: 100%;
+  height: 100%;
+}
+
+.picker-view-item {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 80rpx;
+  font-size: 32rpx;
+  color: #333333;
+}
+
+.picker-item-text {
+  font-size: 32rpx;
+  color: #333333;
+  line-height: 80rpx;
+  height: 80rpx;
+  text-align: center;
+}
+
+/* 添加加载中样式 */
+.picker-loading {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 30rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.picker-empty {
+  padding: 30rpx 0;
+  text-align: center;
+  color: #CCCCCC;
+  font-size: 28rpx;
+}
+</style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 420 - 458
pages/activity/activity-detail.vue


+ 1770 - 0
pages/activity/activity-detail.vue.backup

@@ -0,0 +1,1770 @@
+<template>
+  <view class="page-container">
+    <!-- 页面滚动区域 -->
+    <scroll-view class="page-scroll" scroll-y>
+      <!-- 地块信息卡片 -->
+      <view class="info-card">
+        <view class="card-title">
+          <text>地块信息</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">地块名称</text>
+          <text class="info-value">{{ formData.fieldName || '未知' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">作物名称</text>
+          <text class="info-value">{{ formData.growCrops || '未知' }}</text>
+        </view>
+        <view class="info-item">
+          <text class="info-label">负责人</text>
+          <text class="info-value">{{ formData.manager || '未知' }}</text>
+        </view>
+      </view>
+
+      <!-- 任务填写表单 -->
+      <view class="form-card">
+        <view class="card-title">
+          <text>任务信息</text>
+        </view>
+        
+        <!-- 任务名称 -->
+        <view class="form-item">
+          <view class="form-label required">任务名称</view>
+          <input 
+            v-model="formData.taskName"
+            placeholder="请输入任务名称,例如:水稻田施肥"
+            :disabled="pageMode === 'view'"
+            class="form-input"
+          />
+        </view>
+
+        <!-- 任务类型 -->
+        <view class="form-item" @click="pageMode !== 'view' && showTaskTypeSelector()">
+          <view class="form-label required">任务类型</view>
+          <view class="select-wrapper">
+<!--            <input 
+              :value="formData.typeName"
+              :placeholder="dictLoading ? '加载中...' : '请选择任务类型'"
+			  readonly
+              class="form-input select-input"
+            /> -->
+			<view class="form-input select-input">
+			      {{ formData.typeName || (dictLoading ? '加载中...' : '请选择任务类型') }}
+			</view>
+            <view v-if="pageMode !== 'view'" class="select-arrow">
+              <text>▼</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 执行时间 -->
+        <view class="form-item" @click="pageMode !== 'view' && selectExecuteTime()">
+          <view class="form-label required">执行时间</view>
+          <view class="select-wrapper">
+<!--            <input 
+              :value="formattedExecuteTime"
+              placeholder="请选择任务计划时间"
+              readonly
+              class="form-input select-input"
+              readonly
+            /> -->
+			<view class="form-input select-input">
+			      {{ formattedExecuteTime || '请选择任务计划时间' }}
+			</view>
+            <view v-if="pageMode !== 'view'" class="select-arrow">
+              <text>选择</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 负责人选择 -->
+        <view class="form-item" @click="pageMode !== 'view' && showUserSelector()">
+          <view class="form-label required">负责人</view>
+          <view class="select-wrapper">
+<!--            <input 
+              :value="formData.assigneeName"
+              placeholder="请选择负责人"
+			  readonly
+              class="form-input select-input"
+            /> -->
+			<view class="form-input select-input">
+			      {{ formData.assigneeName || '请选择负责人' }}
+			</view>
+            <view v-if="pageMode !== 'view'" class="select-arrow">
+              <text>选择</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 任务说明 -->
+        <view class="form-item">
+          <view class="form-label">任务说明</view>
+          <textarea 
+            v-model="formData.remark"
+            placeholder="请输入任务要点,例如:每亩用肥20kg"
+            :disabled="pageMode === 'view'"
+            class="form-textarea"
+            maxlength="200"
+          ></textarea>
+          <view class="char-count">{{ (formData.remark || '').length }}/200</view>
+        </view>
+
+        <!-- 任务完成情况 -->
+        <view class="section-divider"></view>
+        <view class="section-title">
+          <text>任务完成情况</text>
+        </view>
+
+        <!-- 完成状态选择 - 新建和编辑模式 -->
+        <view class="form-item" v-if="pageMode === 'create' || pageMode === 'edit'">
+          <view class="form-label">完成状态</view>
+          <view class="radio-group">
+            <view 
+              class="radio-item" 
+              v-for="(item, index) in completionStatusOptions" 
+              :key="index"
+              @click="selectCompletionStatus(item.value)"
+            >
+              <view class="radio-circle" :class="{'radio-checked': formData.completionStatus === item.value}">
+                <view v-if="formData.completionStatus === item.value" class="radio-dot"></view>
+              </view>
+              <text class="radio-label">{{ item.label }}</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 查看模式显示完成状态 -->
+        <view class="form-item" v-if="pageMode === 'view'">
+          <view class="form-label">完成状态</view>
+          <view class="status-completed">
+            <text class="status-icon">✓</text>
+            <text>已完成</text>
+          </view>
+        </view>
+
+        <!-- 完成时间 -->
+        <view class="form-item" v-if="formData.completionStatus === '1'">
+          <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成时间</view>
+                     <view class="select-wrapper" @click="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1' && selectCompletionTime()">
+            <!-- <input 
+              :value="formattedCompletionTime"
+              placeholder="请选择实际完成时间"
+              readonly
+              class="form-input select-input"
+            /> -->
+			<view class="form-input select-input">
+			      {{ formattedCompletionTime || '请选择实际完成时间' }}
+			</view>
+                <view v-if="(pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'" class="select-arrow">
+               <text>选择</text>
+             </view>
+          </view>
+        </view>
+
+        <!-- 完成说明 -->
+        <view class="form-item" v-if="formData.completionStatus === '1'">
+          <view class="form-label" :class="{'required': (pageMode === 'create' || pageMode === 'edit') && formData.completionStatus === '1'}">完成说明</view>
+          <textarea 
+            v-model="formData.completionDesc"
+            placeholder="请输入完成说明,例如:已完成并拍照记录"
+            :disabled="pageMode === 'view'"
+            class="form-textarea"
+            maxlength="300"
+          ></textarea>
+          <view class="char-count">{{ (formData.completionDesc || '').length }}/300</view>
+          <view class="form-error" v-if="formErrors.completionDesc">
+            {{ formErrors.completionDesc }}
+          </view>
+        </view>
+
+        <!-- 现场图片 -->
+        <view class="form-item" v-if="formData.completionStatus === '1'">
+          <view class="form-label">现场图片</view>
+          
+          <!-- 新建和编辑模式 -->
+          <view v-if="pageMode === 'create' || pageMode === 'edit'" class="image-upload">
+            <view class="image-list">
+              <view 
+                class="image-preview" 
+                v-for="(item, index) in formData.images" 
+                :key="index"
+                @click="previewImage(item, index)"
+              >
+                <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
+                <image :src="item.url" mode="aspectFill"/>
+                <view class="delete-btn" @click.stop="deletePic(index)">
+                  <text>×</text>
+                </view>
+              </view>
+              <view 
+                v-if="formData.images.length < 6" 
+                class="upload-btn" 
+                @click="chooseImage"
+              >
+                <text class="upload-icon">+</text>
+                <text class="upload-text">添加图片</text>
+              </view>
+            </view>
+            <view class="upload-tip">最多可上传6张图片</view>
+          </view>
+
+          <!-- 查看模式 -->
+          <view v-else-if="pageMode === 'view' && formData.images && formData.images.length > 0" class="image-view">
+            <view class="image-list">
+              <view 
+                class="image-preview" 
+                v-for="(item, index) in formData.images" 
+                :key="index"
+                @click="previewImage(item, index)"
+              >
+                <!-- <image :src="getImageUrl(item)" mode="aspectFill"/> -->
+                <image :src="item.url" mode="aspectFill"/>
+              </view>
+            </view>
+          </view>
+
+          <!-- 无图片提示 -->
+          <view v-else class="no-images">
+            <text>暂无图片</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 底部占位 -->
+      <view class="bottom-safe"></view>
+    </scroll-view>
+
+    <!-- 底部提交按钮 -->
+    <view class="footer-safe" v-if="pageMode !== 'view'">
+      <view class="footer-content">
+        <button 
+          class="submit-button"
+          :class="{'loading': isSubmitting}"
+          @click="submitForm"
+          :disabled="isSubmitting"
+        >
+          {{ isSubmitting ? '提交中...' : submitButtonText }}
+        </button>
+      </view>
+    </view>
+
+
+
+    <!-- 遮罩层 -->
+    <view v-if="showTaskTypePicker || showDateTimeSelector || showUserPicker" class="picker-mask" @click="closePickers"></view>
+    
+    <!-- 任务类型选择弹窗 -->
+    <view v-if="showTaskTypePicker" class="picker-popup">
+      <view class="picker-header">
+        <text class="picker-cancel" @click="showTaskTypePicker = false">取消</text>
+        <text class="picker-title">选择任务类型</text>
+        <text class="picker-confirm" @click="confirmTaskType">确定</text>
+      </view>
+      <view class="picker-content">
+        <view v-if="dictLoading" class="picker-loading">
+          <text>加载中...</text>
+        </view>
+        <view 
+          v-else
+          class="picker-item" 
+          v-for="(item, index) in taskTypeOptions" 
+          :key="index"
+          :class="{'selected': tempTaskTypeIndex === index}"
+          @click="tempTaskTypeIndex = index"
+        >
+          <text>{{ item.dictLabel }}</text>
+          <text v-if="tempTaskTypeIndex === index" class="check-mark">✓</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 用户选择弹窗 -->
+    <view v-if="showUserPicker" class="picker-popup">
+      <view class="picker-header">
+        <text class="picker-cancel" @click="showUserPicker = false">取消</text>
+        <text class="picker-title">选择负责人</text>
+        <text class="picker-confirm" @click="confirmUser">确定</text>
+      </view>
+      <view class="picker-content">
+        <view v-if="usersLoading" class="picker-loading">
+          <text>加载中...</text>
+        </view>
+        <view v-else-if="userList.length === 0" class="picker-empty">
+          <text>暂无可选负责人</text>
+        </view>
+        <view 
+          v-else
+          class="picker-item" 
+          v-for="(user, index) in userList" 
+          :key="user.userId"
+          :class="{'selected': tempUserIndex === index}"
+          @click="tempUserIndex = index"
+        >
+          <text>{{ user.userName }}</text>
+          <text v-if="tempUserIndex === index" class="check-mark">✓</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 日期时间选择弹窗 -->
+    <view v-if="showDateTimeSelector" class="picker-popup datetime-picker">
+      <view class="picker-header">
+        <text class="picker-cancel" @click="cancelDateTime">取消</text>
+        <text class="picker-title">选择时间</text>
+        <text class="picker-confirm" @click="confirmDateTime">确定</text>
+      </view>
+      <view class="datetime-picker-content">
+        <picker-view 
+          class="datetime-picker-view"
+          :value="dateTimePickerValue" 
+          @change="onDateTimePickerChange"
+          :indicator-style="'height: 80rpx;'"
+          :mask-style="'background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6)), linear-gradient(0deg, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0.6));'"
+        >
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[0]" :key="'year-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[1]" :key="'month-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[2]" :key="'day-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[3]" :key="'hour-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+          <picker-view-column>
+            <view class="picker-view-item" v-for="(item, index) in dateTimePickerRange[4]" :key="'min-'+index">
+              <text class="picker-item-text">{{ item }}</text>
+            </view>
+          </picker-view-column>
+        </picker-view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import api from "@/config/api.js";
+import { getAgriculturalTasksById, addAgriculturalTask, updateAgriculturalTask } from '@/api/services/activity.js';
+import { getUsersByPlotId, getUserInfo } from '@/api/services/user.js';
+import { useDict } from '@/utils/composables/useDict';
+import storage from "@/utils/storage.js";
+
+// 使用字典组合式函数
+const { dictData, dictLoading, loadDict } = useDict(['task_type', 'task_status'])
+
+// 页面模式:create-新建, edit-编辑, view-查看
+const pageMode = ref('create')
+
+// 选择器显示状态
+const showTaskTypePicker = ref(false)
+const showDateTimeSelector = ref(false)
+const showUserPicker = ref(false)
+
+// 临时选择的任务类型索引
+const tempTaskTypeIndex = ref(0)
+const tempUserIndex = ref(0)
+
+// 时间选择器相关
+const currentTimeType = ref('')
+const dateTimePickerValue = ref([0, 0, 0, 0, 0])
+const dateTimePickerRange = ref([])
+
+// 表单数据
+const formData = reactive({
+  // 任务ID,新建任务时为空
+  id: '',
+  
+  // 地块基础信息
+  plotId: '',
+  fieldName: '',
+  growCrops: '',
+  manager: '',
+  
+  // 任务信息
+  taskName: '',
+  typeName: '',
+  typeNameId: '',
+  executeTime: new Date(),
+  remark: '',
+  
+  // 完成情况
+  taskStatus: '0',
+  completionStatus: '0',
+  completionTime: new Date(),
+  completionDesc: '',
+  images: [],
+  taskImages: '',
+  
+  // 负责人信息
+  assigneeId: '',
+  assigneeName: '',
+  
+  // 创建人信息
+  create_by: ''
+})
+
+// 完成状态选项
+const completionStatusOptions = [
+  { label: '待完成', value: '0' },
+  { label: '已完成', value: '1' }
+]
+
+// 是否正在提交
+const isSubmitting = ref(false)
+
+// 表单验证错误
+const formErrors = reactive({
+  completionDesc: ''
+})
+
+// 用户列表
+const userList = ref([])
+const usersLoading = ref(false)
+
+// 任务类型列表
+const taskStatusList = ref([])
+
+// 计算属性
+const pageTitle = computed(() => {
+  switch (pageMode.value) {
+    case 'create':
+      return '新建农事任务'
+    case 'edit':
+      return '编辑农事任务'
+    default:
+      return '农事任务详情'
+  }
+})
+
+const submitButtonText = computed(() => {
+  switch (pageMode.value) {
+    case 'create':
+      return '创建任务'
+    case 'edit':
+      return '保存修改'
+    default:
+      return ''
+  }
+})
+
+const formattedExecuteTime = computed(() => {
+  return formatDateTime(formData.executeTime)
+})
+
+const formattedCompletionTime = computed(() => {
+  return formatDateTime(formData.completionTime)
+})
+
+const taskTypeOptions = computed(() => {
+  console.log("dictData:", dictData.value.task_type)
+  return dictData.value.task_type
+})
+
+const taskTypeIndex = computed(() => {
+  console.log("taskTypeOptions", taskTypeOptions.value)
+  const index = taskTypeOptions.value.findIndex(item => item.dictValue === formData.typeNameId)
+  console.log("查找:", index)
+  return index >= 0 ? index : 0
+})
+
+// 初始化字典数据
+loadDict().then(() => {
+  // 字典加载完成后,如果没有设置任务类型,则默认选择第一个
+  if (!formData.typeName && dictData.value.task_type && dictData.value.task_type.length > 0) {
+    const firstType = dictData.value.task_type[0]
+    taskStatusList.value = dictData.value.task_type
+    console.log("dictData", dictData.value.task_status[0].dictLabel)
+    formData.typeName = firstType.dictLabel
+    formData.typeNameId = firstType.dictValue
+  }
+})
+
+// uni-app 生命周期
+const onLoad = (options) => {
+  console.log('页面加载,接收参数:', options)
+  
+  // 设置页面模式
+  if (options.mode) {
+    pageMode.value = options.mode
+  }
+  
+  // 设置地块基础信息
+  formData.fieldName = decodeURIComponent(options.fieldName === 'undefined' ? '未选择地块' : options.fieldName)
+  formData.growCrops = decodeURIComponent(options.growCrops === 'undefined' ? '未选择地块' : options.growCrops)
+  formData.manager = decodeURIComponent(options.manager === 'undefined' ? '未选择地块' : options.manager)
+  formData.plotId = parseInt(options.plotId || '1')
+  formData.farmId = parseInt(options.farmId || '1')
+  
+  // 加载用户列表
+  loadUserList(formData.farmId)
+  
+  // 如果是编辑或查看模式,获取任务详情
+  if (options.id && options.id !== 'new') {
+    formData.id = options.id
+    loadTaskDetail(options.id)
+  } else if (options.id === 'new') {
+    // 加载当前登录用户
+    loadAssigneeInfo()
+  }
+  
+  // 设置导航栏标题
+  uni.setNavigationBarTitle({
+    title: pageTitle.value
+  })
+  
+  // 初始化时间选择器数据
+  initDateTimeRange()
+}
+
+// 方法定义
+// 跨端安全解析日期
+const normalizeToDate = (input) => {
+  if (!input) return new Date()
+  let dateObj = input
+  if (typeof input === 'string') {
+    // 兼容 iOS:将 2025-08-12 12:30:00 转为 2025/08/12 12:30:00
+    dateObj = input.replace(/-/g, '/')
+  }
+  if (typeof input === 'number') {
+    // 秒级时间戳转毫秒
+    if (input.toString().length === 10) {
+      dateObj = input * 1000
+    }
+  }
+  const d = new Date(dateObj)
+  return isNaN(d.getTime()) ? new Date() : d
+}
+
+// 加载任务详情
+const loadTaskDetail = (taskId) => {
+  uni.showLoading({
+    title: '加载中...',
+    mask: true
+  })
+  
+  getAgriculturalTasksById(taskId).then(res => {
+    if (res.data.code === 200) {
+      const taskDetail = res.data.data
+      // 设置表单数据
+      Object.assign(formData, {
+        ...taskDetail,
+        manager: formData.manager,
+        completionStatus: taskDetail.taskStatus
+      })
+      
+      // 转换任务类型值
+      if (taskStatusList.value && taskStatusList.value.length > 0) {
+        const typeDict = taskStatusList.value.find(item => item.dictValue === taskDetail.typeName.toString())
+        if (typeDict) {
+          formData.typeName = typeDict.dictLabel
+          formData.typeNameId = typeDict.dictValue
+        }
+      }
+      
+      // 处理图片数据
+      if (formData.taskImages) {
+        try {
+          const imageUrls = formData.taskImages.split(',')
+          formData.images = imageUrls.filter(url => url && url.trim()).map(url => ({
+            url: url.trim(),
+            status: 'success'
+          }))
+          console.log('解析后的图片数据:', formData.images)
+        } catch (e) {
+          console.error('解析图片数据失败:', e)
+          formData.images = []
+        }
+      } else {
+        formData.images = []
+      }
+      console.log("formData.assigneeId", formData.assigneeId)
+      console.log("!formData.assigneeName", formData.assigneeName)
+      // 如果有负责人ID但没有负责人名称,获取负责人信息
+      if (formData.assigneeId && !formData.assigneeName) {
+        loadAssigneeInfo(formData.assigneeId)
+      }
+          
+          uni.hideLoading();
+        } else {
+          uni.hideLoading();
+          uni.showToast({
+            title: res.data.msg || '获取任务详情失败',
+            icon: 'none'
+          });
+          
+          // 失败后返回上一页
+          setTimeout(() => {
+            uni.navigateBack();
+          }, 1500);
+        }
+      }).catch(err => {
+        console.error('获取任务详情失败:', err);
+        uni.hideLoading();
+        uni.showToast({
+          title: '获取任务详情失败',
+          icon: 'none'
+        });
+        
+        // 失败后返回上一页
+        setTimeout(() => {
+          uni.navigateBack();
+        }, 1500);
+      });
+    },
+    
+    // 加载负责人信息
+    loadAssigneeInfo(userId) {
+      getUserInfo(userId).then(res => {
+		  console.log("你是谁:",res);
+        if (res.data.code === 200) {
+          const userData = res.data.data;
+          this.formData.assigneeName = userData.userName || '未知用户';
+          this.formData.assigneeId = userData.userId;
+        }
+      }).catch(err => {
+        console.error('获取负责人信息失败:', err);
+        this.formData.assigneeName = '未知用户';
+      });
+    },
+
+    // 加载用户列表
+    loadUserList(farmId) {
+      this.usersLoading = true;
+      const params = {
+		  pageNum: 1,
+		  pageSize: 10,
+		  deptId: farmId
+	  }
+      getUsersByPlotId(params).then(res => {
+		  console.log("加载用户:",res);
+        if (res.data.code === 200) {
+          this.userList = res.data.rows || [];
+          
+          // 设置默认选中第一个用户
+          if (this.userList.length > 0 && !this.formData.assigneeId) {
+            this.formData.assigneeId = this.userList[0].userId;
+            this.formData.assigneeName = this.userList[0].userName;
+          }
+        } else {
+          console.error('获取用户列表失败:', res.data.msg);
+          uni.showToast({
+            title: '获取用户列表失败',
+            icon: 'none'
+          });
+        }
+      }).catch(err => {
+        console.error('获取用户列表失败:', err);
+        uni.showToast({
+          title: '获取用户列表失败',
+          icon: 'none'
+        });
+      }).finally(() => {
+        this.usersLoading = false;
+      });
+    },
+    
+    // 初始化时间选择器范围数据
+    initDateTimeRange() {
+      // 年份:从2020年开始显示10年
+      const startYear = 2020;
+      const years = [];
+      for (let i = 0; i < 10; i++) {
+        years.push((startYear + i) + '年');
+      }
+      
+      // 月份:1-12月
+      const months = [];
+      for (let i = 1; i <= 12; i++) {
+        months.push(i + '月');
+      }
+      
+      // 日期:1-31日
+      const days = [];
+      for (let i = 1; i <= 31; i++) {
+        days.push(i + '日');
+      }
+      
+      // 小时:0-23时
+      const hours = [];
+      for (let i = 0; i <= 23; i++) {
+        hours.push(i.toString().padStart(2, '0') + '时');
+      }
+      
+      // 分钟:0-59分
+      const minutes = [];
+      for (let i = 0; i <= 59; i++) {
+        minutes.push(i.toString().padStart(2, '0') + '分');
+      }
+      
+      this.dateTimePickerRange = [years, months, days, hours, minutes];
+      console.log('日期选择器范围:', {
+        years: years.length,
+        months: months.length,
+        days: days.length,
+        hours: hours.length,
+        minutes: minutes.length
+      });
+    },
+    
+    // 初始化日期时间选择器
+    initDateTimePicker(timestamp) {
+      const date = this.normalizeToDate(timestamp);
+      const currentYear = date.getFullYear();
+      const currentMonth = date.getMonth() + 1; // 1-12
+      const currentDate = date.getDate(); // 1-31
+      const currentHour = date.getHours();
+      const currentMinute = date.getMinutes();
+      
+      // 基准年份,用于计算索引
+      const startYear = 2020;
+      
+      // 如果当前年份小于起始年份或大于结束年份,调整为合法范围内
+      let yearIndex = currentYear - startYear;
+      if (yearIndex < 0) yearIndex = 0;
+      if (yearIndex >= 10) yearIndex = 9;
+      
+      console.log('初始化日期选择器:', {
+        timestamp,
+        year: currentYear,
+        month: currentMonth,
+        date: currentDate,
+        hour: currentHour,
+        minute: currentMinute,
+        yearIndex
+      });
+      
+      // 设置选择器的初始值
+      this.dateTimePickerValue = [
+        yearIndex, // 年份索引
+        currentMonth - 1, // 月份索引(0-11)
+        currentDate - 1, // 日期索引(0-30)
+        currentHour, // 小时索引
+        currentMinute // 分钟索引
+      ];
+      
+      console.log('选择器初始值:', this.dateTimePickerValue);
+      
+      // 更新日期范围
+      this.updateDaysRange();
+    },
+    
+    // 更新日期范围(根据选择的年月)
+    updateDaysRange() {
+      const yearIndex = this.dateTimePickerValue[0];
+      const monthIndex = this.dateTimePickerValue[1];
+      
+      // 计算实际年月
+      const year = 2020 + yearIndex;
+      const month = monthIndex + 1; // 索引为0-11,实际月份为1-12
+      
+      // 获取该月的天数
+      const daysInMonth = new Date(year, month, 0).getDate();
+      
+      // 更新天数范围
+      const days = [];
+      for (let i = 1; i <= daysInMonth; i++) {
+        days.push(i + '日');
+      }
+      
+      // 更新日期列
+      this.dateTimePickerRange[2] = days;
+      
+      // 如果当前选择的日期超过了该月的天数,调整为该月最后一天
+      if (this.dateTimePickerValue[2] >= daysInMonth) {
+        this.dateTimePickerValue[2] = daysInMonth - 1;
+      }
+      
+      console.log('更新日期范围:', {
+        year,
+        month,
+        daysInMonth,
+        daysLength: days.length
+      });
+    },
+    
+    // 显示任务类型选择器
+    showTaskTypeSelector() {
+      this.tempTaskTypeIndex = this.taskTypeIndex;
+      this.showTaskTypePicker = true;
+    },
+    
+    // 确认任务类型选择
+    confirmTaskType() {
+      const selectedType = this.taskTypeOptions[this.tempTaskTypeIndex];
+	  console.log("选中:",selectedType);
+      this.formData.typeName = selectedType.dictLabel;
+      this.formData.typeNameId = selectedType.dictValue; // 设置typeNameId为dictValue值
+      this.showTaskTypePicker = false;
+    },
+    
+    // 选择执行时间
+    selectExecuteTime() {
+      this.currentTimeType = 'executeTime';
+      this.initDateTimePicker(this.formData.executeTime);
+      this.showDateTimeSelector = true;
+    },
+    
+    // 选择完成时间
+    selectCompletionTime() {
+      this.currentTimeType = 'completionTime';
+      this.initDateTimePicker(this.formData.completionTime);
+      this.showDateTimeSelector = true;
+    },
+    
+    // 日期时间选择器变更
+    onDateTimePickerChange(e) {
+      this.dateTimePickerValue = e.detail.value;
+      // 当年月变更时,更新日期范围
+      this.updateDaysRange();
+    },
+    
+    // 确认时间选择
+    confirmDateTime() {
+      const values = this.dateTimePickerValue;
+      
+      // 计算实际日期时间
+      const year = 2020 + values[0];
+      const month = values[1] + 1; // 索引为0-11,实际月份为1-12
+      const date = values[2] + 1; // 索引为0-30,实际日期为1-31
+      const hour = values[3];
+      const minute = values[4];
+      
+      console.log('确认日期时间:', {
+        selectedValues: values,
+        convertedDate: `${year}-${month}-${date} ${hour}:${minute}`
+      });
+      
+      // 创建日期对象
+      const selectedTime = new Date(year, month - 1, date, hour, minute).getTime();
+      
+      // 更新相应的表单字段
+      if (this.currentTimeType === 'executeTime') {
+        this.formData.executeTime = selectedTime;
+      } else if (this.currentTimeType === 'completionTime') {
+        this.formData.completionTime = selectedTime;
+      }
+      
+      // 关闭选择器
+      this.showDateTimeSelector = false;
+      
+      uni.showToast({
+        title: '时间已设置',
+        icon: 'success',
+        duration: 1500
+      });
+    },
+    
+    // 取消时间选择
+    cancelDateTime() {
+      this.showDateTimeSelector = false;
+    },
+
+    // 显示用户选择器
+    showUserSelector() {
+      this.tempUserIndex = this.userList.findIndex(user => user.userId === this.formData.assigneeId);
+      this.showUserPicker = true;
+    },
+
+    // 确认用户选择
+    confirmUser() {
+      const selectedUser = this.userList[this.tempUserIndex];
+      this.formData.assigneeId = selectedUser.userId;
+      this.formData.assigneeName = selectedUser.userName;
+      this.showUserPicker = false;
+    },
+    
+    // 关闭所有选择器
+    closePickers() {
+      this.showTaskTypePicker = false;
+      this.showDateTimeSelector = false;
+      this.showUserPicker = false;
+    },
+    
+    // 选择完成状态
+    selectCompletionStatus(value) {
+      this.formData.completionStatus = value;
+      this.formData.taskStatus = value; // 同时设置taskStatus字段
+      
+      if (value === '1') {
+        this.formData.completionTime = new Date();
+      }
+    },
+    
+    // 格式化日期时间
+	/**
+	 * 日期格式化工具(兼容 iOS 和安卓)
+	 * @param {string|number|Date} date - 日期对象 / 时间戳 / 日期字符串
+	 * @param {string} format - 格式模板,默认 'yyyy-MM-dd HH:mm:ss'
+	 * @returns {string} 格式化后的日期
+	 */
+	formatDateTime(date, format = 'yyyy-MM-dd HH:mm') {
+	  if (!date) return '';
+	
+	  // 如果是字符串,做 iOS 兼容(将 2025-08-12 替换为 2025/08/12)
+	  if (typeof date === 'string') {
+	    date = date.replace(/-/g, '/');
+	  }
+	
+	  // 如果是数字(时间戳),判断是否是秒级
+	  if (typeof date === 'number') {
+	    if (date.toString().length === 10) {
+	      date *= 1000; // 秒转毫秒
+	    }
+	  }
+	
+	  // 转换为 Date 对象
+	  date = new Date(date);
+	  if (isNaN(date.getTime())) return '';
+	
+	  const map = {
+	    'yyyy': date.getFullYear(),
+	    'MM': String(date.getMonth() + 1).padStart(2, '0'),
+	    'dd': String(date.getDate()).padStart(2, '0'),
+	    'HH': String(date.getHours()).padStart(2, '0'),
+	    'mm': String(date.getMinutes()).padStart(2, '0'),
+	    // 'ss': String(date.getSeconds()).padStart(2, '0')
+	  };
+	
+	  return format.replace(/yyyy|MM|dd|HH|mm/g, match => map[match]);
+	},
+	// formatDateTime(timestamp) {
+	//   if (!timestamp) return '';
+	//   const date = parseDate(timestamp);
+	//   if (isNaN(date.getTime())) return '';
+	//   const year = date.getFullYear();
+	//   const month = String(date.getMonth() + 1).padStart(2, '0');
+	//   const day = String(date.getDate()).padStart(2, '0');
+	//   const hour = String(date.getHours()).padStart(2, '0');
+	//   const minute = String(date.getMinutes()).padStart(2, '0');
+	//   return `${year}-${month}-${day} ${hour}:${minute}`;
+	// },
+
+    
+    // 选择图片
+    chooseImage() {
+      uni.chooseImage({
+        count: 6 - this.formData.images.length,
+        sizeType: ['original', 'compressed'],
+        sourceType: ['album', 'camera'],
+        success: (res) => {
+          console.log('选择图片成功:', res);
+          
+          // 验证文件类型和大小
+          const validFiles = [];
+          const invalidFiles = [];
+          const maxSize = 5 * 1024 * 1024; // 5MB 最大限制
+          
+          // 检查每个文件
+          res.tempFiles.forEach((file, index) => {
+            // 检查文件类型
+            const isImage = /\.(jpg|jpeg|png|gif)$/i.test(file.name);
+            // 检查文件大小
+            const isValidSize = file.size <= maxSize;
+            
+            if (isImage && isValidSize) {
+              validFiles.push(res.tempFilePaths[index]);
+            } else {
+              invalidFiles.push({
+                path: file.path,
+                size: file.size,
+                reason: !isImage ? '文件格式不支持' : '文件大于5MB'
+              });
+            }
+          });
+          
+          // 显示无效文件提示
+          if (invalidFiles.length > 0) {
+            uni.showToast({
+              title: `${invalidFiles.length}个文件无效,请检查格式和大小`,
+              icon: 'none',
+              duration: 2000
+            });
+          }
+          
+          // 如果有有效文件,则上传
+          if (validFiles.length > 0) {
+            this.uploadImages(validFiles);
+          }
+        },
+        fail: (err) => {
+          console.error('选择图片失败:', err);
+          uni.showToast({
+            title: '选择图片失败',
+            icon: 'none'
+          });
+        }
+      });
+    },
+
+    // 上传图片到服务器
+    uploadImages(tempFilePaths) {
+      uni.showLoading({
+        title: '上传中...',
+        mask: true
+      });
+      
+      // 上传成功的图片计数
+      let successCount = 0;
+      let failCount = 0;
+      const totalFiles = tempFilePaths.length;
+      const newImages = [];
+      
+      // 遍历处理每张图片
+      tempFilePaths.forEach((path, index) => {
+        // 调用上传API
+        uni.uploadFile({
+          // url: api.serve + '/base/tasks/uploadTaskImage', 
+          url: api.serve + '/file/upload', 
+          filePath: path,
+          name: 'file', // 文件参数名称,需要与后端接口匹配
+          formData: {
+            type: 'task', // 标识文件类型,用于后端区分不同业务的文件
+            // directory: '/opt/app/nongxiaoyu/uploadImage' // 指定保存目录
+          },
+		  header: {
+		  	'Authorization': `Bearer ${storage.getAccessToken()}`
+		  },
+          success: (res) => {
+                          try {
+                const response = JSON.parse(res.data);
+				uni.showToast({
+				  title: `返回: ${response.data}`,
+				  icon: 'none',
+				});
+                if (response.code === 200) {
+                  // 获取返回的URL
+                  const imageUrl = response.data.url;
+                  console.log('上传成功,返回的图片URL:', imageUrl);
+                  
+                  // 上传成功,将图片信息添加到数组
+                  newImages.push({
+                    url: imageUrl, // 保存原始URL,在显示时会通过getImageUrl方法处理
+                    path: path, // 保存本地路径用于预览
+                    status: 'success',
+                    fileName: response.data.fileName || '' // 保存文件名,如果后端返回的话
+                  });
+                  successCount++;
+              } else {
+                failCount++;
+                console.error('上传失败:', response.msg);
+              }
+            } catch (e) {
+              failCount++;
+			  uni.showToast({
+			    title: `解析响应失败: ${e}`,
+			    icon: 'none',
+			  });
+              console.error('解析响应失败:', e);
+            }
+          },
+          fail: (err) => {
+            failCount++;
+            console.error('上传请求失败:', err);
+			uni.showToast({
+			  title: `上传请求失败: ${err}`,
+			  icon: 'none',
+			});
+          },
+          complete: () => {
+            // 当所有文件都已处理完成
+            if (successCount + failCount === totalFiles) {
+              if (newImages.length > 0) {
+                // 将新上传的图片添加到已有图片列表
+                this.formData.images = [...this.formData.images, ...newImages];
+                console.log("this.formData.images",this.formData.images);
+                // 更新taskImages字段,将图片URL用逗号连接
+                this.formData.taskImages = this.formData.images.map(img => img.url).join(',');
+                
+                // 显示成功提示
+                uni.hideLoading();
+                uni.showToast({
+                  title: `成功上传${successCount}张图片`,
+                  icon: 'success'
+                });
+              } else {
+                // 全部失败
+				uni.showToast({
+				  title: `图片上传: ${newImages}`,
+				  icon: 'none',
+				});
+                uni.hideLoading();
+                // uni.showToast({
+                //   title: '图片上传失败',
+                //   icon: 'none',
+                // });
+              }
+            }
+          }
+        });
+      });
+    },
+
+    // 删除图片
+    deletePic(index) {
+      uni.showModal({
+        title: '确认删除',
+        content: '确定要删除这张图片吗?',
+        success: (res) => {
+          if (res.confirm) {
+            this.formData.images.splice(index, 1);
+            
+            // 更新taskImages字段
+            this.formData.taskImages = this.formData.images.map(img => img.url).join(',');
+            
+            uni.showToast({
+              title: '已删除',
+              icon: 'none'
+            });
+          }
+        }
+      });
+    },
+
+    // 获取图片URL
+    getImageUrl(item) {
+      // 默认返回url或path
+      return  api.upload + item.url;
+    },
+    
+    // 预览图片
+    previewImage(item, index) {
+      // 获取所有图片的完整URL
+      // const urls = this.formData.images.map(file => this.getImageUrl(file));
+	  const urls = this.formData.images.map(item => item.url);
+      uni.previewImage({
+        urls: urls,
+        current: index
+      });
+    },
+    
+    // 表单验证
+    validateForm() {
+      // 重置错误信息
+      this.formErrors = {
+        completionDesc: ''
+      };
+      
+      // 基本字段验证
+      if (!this.formData.taskName.trim()) {
+        uni.showToast({
+          title: '请输入任务名称',
+          icon: 'none'
+        });
+        return false;
+      }
+      
+      if (!this.formData.typeName) {
+        uni.showToast({
+          title: '请选择任务类型',
+          icon: 'none'
+        });
+        return false;
+      }
+      
+      // 验证负责人
+      if (!this.formData.assigneeId) {
+        uni.showToast({
+          title: '请选择负责人',
+          icon: 'none'
+        });
+        return false;
+      }
+      
+      // 新建和编辑模式且已完成状态下的验证
+      if ((this.pageMode === 'create' || this.pageMode === 'edit') && this.formData.taskStatus === '1') {
+        // 验证完成说明
+        if (!this.formData.completionDesc.trim()) {
+          this.formErrors.completionDesc = '请填写完成说明';
+          uni.showToast({
+            title: '请填写完成说明',
+            icon: 'none'
+          });
+          return false;
+        }
+        
+        // 验证是否上传了图片
+        if (!this.formData.images || this.formData.images.length === 0) {
+          uni.showToast({
+            title: '请上传至少一张现场图片',
+            icon: 'none'
+          });
+          return false;
+        }
+      }
+      
+      return true;
+    },
+    
+    // 提交表单
+    submitForm() {
+      if (!this.validateForm()) {
+        return;
+      }
+      
+      this.isSubmitting = true;
+      
+      // 构建提交数据
+      const submitData = {
+        // 如果是编辑模式,需要提供ID
+        ...(this.formData.id ? { id: this.formData.id } : {}),
+		farmId: this.formData.farmId,
+        plotId: this.formData.plotId,
+        fieldName: this.formData.fieldName,
+        growCrops: this.formData.growCrops,
+        taskName: this.formData.taskName,
+        taskImages: this.formData.taskImages,
+        typeName: this.formData.typeNameId,
+        taskStatus: this.formData.taskStatus,
+        executeTime: this.formatDateTime(this.formData.executeTime),
+        assigneeId: this.formData.assigneeId,
+		assigneeName:this.formData.assigneeName,
+        remark: this.formData.remark,
+        completionTime: this.formData.taskStatus === '1' ? this.formatDateTime(this.formData.completionTime) : null,
+        completionDesc: this.formData.taskStatus === '1' ? this.formData.completionDesc : null,
+        create_by: this.formData.create_by || null
+      };
+      
+      console.log('提交数据:', submitData);
+      
+      uni.showLoading({
+        title: '提交中...',
+        mask: true
+      });
+      
+      // 根据模式决定是创建还是更新
+      const requestPromise = this.pageMode === 'create' 
+        ? addAgriculturalTask(submitData) 
+        : updateAgriculturalTask(submitData);
+      
+      requestPromise.then(res => {
+        this.isSubmitting = false;
+        uni.hideLoading();
+        
+        if (res.data.code === 200) {
+          const successMessage = this.pageMode === 'create' ? '任务创建成功' : '保存修改成功';
+          
+          uni.showToast({
+            title: successMessage,
+            icon: 'success',
+            duration: 1500
+          });
+          
+          setTimeout(() => {
+            uni.navigateBack();
+          }, 1500);
+        } else {
+          uni.showToast({
+            title: res.data.msg || '操作失败',
+            icon: 'none'
+          });
+        }
+      }).catch(err => {
+        console.error('提交表单失败:', err);
+        this.isSubmitting = false;
+        uni.hideLoading();
+        
+        uni.showToast({
+          title: '操作失败,请稍后重试',
+          icon: 'none'
+        });
+      });
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page-container {
+  height: 100vh;
+  background-color: #F5F5F5;
+  display: flex;
+  flex-direction: column;
+}
+
+.page-scroll {
+  flex: 1;
+  overflow: hidden;
+}
+
+/* 地块基础信息卡片 */
+.info-card {
+  background: #F9F9F9;
+  margin: 24rpx;
+  padding: 24rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid #E5E5E5;
+}
+
+.card-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333333;
+  margin-bottom: 16rpx;
+  padding-bottom: 12rpx;
+  border-bottom: 1rpx solid #E5E5E5;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+  padding: 8rpx 0;
+}
+
+.info-item:last-child {
+  margin-bottom: 0;
+}
+
+.info-label {
+  font-size: 28rpx;
+  color: #666666;
+  flex-shrink: 0;
+}
+
+.info-value {
+  font-size: 28rpx;
+  color: #333333;
+  font-weight: 500;
+}
+
+/* 任务填写表单 */
+.form-card {
+  background: #FFFFFF;
+  margin: 0 24rpx 24rpx;
+  padding: 24rpx;
+  border-radius: 8rpx;
+  border: 1rpx solid #E5E5E5;
+}
+
+.section-divider {
+  height: 1rpx;
+  background: #F0F0F0;
+  margin: 32rpx 0 24rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333333;
+  margin-bottom: 16rpx;
+  padding-bottom: 12rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+
+.form-item {
+  margin-bottom: 24rpx;
+}
+
+.form-item:last-child {
+  margin-bottom: 0;
+}
+
+.form-label {
+  font-size: 28rpx;
+  color: #333333;
+  margin-bottom: 12rpx;
+  font-weight: 500;
+}
+
+.form-label.required::before {
+  content: '*';
+  color: #FF6B6B;
+  margin-right: 4rpx;
+}
+
+.form-input {
+  width: 100%;
+  line-height: 80rpx;
+  height: 80rpx;
+  background: #F8F8F8;
+  border: 1rpx solid #E5E5E5;
+  border-radius: 8rpx;
+  padding: 0 16rpx;
+  font-size: 28rpx;
+  color: #333333;
+  box-sizing: border-box;
+}
+
+.form-input:focus {
+  background: #FFFFFF;
+  border-color: #3BB44A;
+}
+
+.form-input::placeholder {
+  color: #CCCCCC;
+}
+
+.form-input:disabled {
+  background: #F8F8F8;
+  color: #999999;
+}
+
+.select-wrapper {
+  position: relative;
+  width: 100%;
+}
+
+.select-input {
+  cursor: pointer;
+}
+
+.select-arrow {
+  position: absolute;
+  right: 16rpx;
+  top: 50%;
+  transform: translateY(-50%);
+  color: #999999;
+  font-size: 20rpx;
+  pointer-events: none;
+}
+
+.form-textarea {
+  width: 100%;
+  min-height: 120rpx;
+  background: #F8F8F8;
+  border: 1rpx solid #E5E5E5;
+  border-radius: 8rpx;
+  padding: 16rpx;
+  font-size: 28rpx;
+  color: #333333;
+  box-sizing: border-box;
+  resize: none;
+  line-height: 1.5;
+}
+
+.form-textarea:focus {
+  background: #FFFFFF;
+  border-color: #3BB44A;
+}
+
+.form-textarea::placeholder {
+  color: #CCCCCC;
+}
+
+.form-textarea:disabled {
+  background: #F8F8F8;
+  color: #999999;
+}
+
+.char-count {
+  font-size: 24rpx;
+  color: #CCCCCC;
+  text-align: right;
+  margin-top: 8rpx;
+}
+
+.form-error {
+  font-size: 24rpx;
+  color: #FF6B6B;
+  margin-top: 8rpx;
+}
+
+/* 单选按钮样式 */
+.radio-group {
+  display: flex;
+  gap: 32rpx;
+}
+
+.radio-item {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+}
+
+.radio-circle {
+  width: 32rpx;
+  height: 32rpx;
+  border: 2rpx solid #CCCCCC;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 8rpx;
+}
+
+.radio-circle.radio-checked {
+  border-color: #3BB44A;
+  background-color: #3BB44A;
+}
+
+.radio-dot {
+  width: 12rpx;
+  height: 12rpx;
+  background-color: #FFFFFF;
+  border-radius: 50%;
+}
+
+.radio-label {
+  font-size: 28rpx;
+  color: #333333;
+}
+
+/* 完成状态样式 */
+.status-completed {
+  display: flex;
+  align-items: center;
+  color: #3BB44A;
+  font-size: 28rpx;
+}
+
+.status-icon {
+  width: 32rpx;
+  height: 32rpx;
+  background: #3BB44A;
+  color: #FFFFFF;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 8rpx;
+  font-size: 20rpx;
+  line-height: 1;
+}
+
+/* 图片上传样式 */
+.image-upload, .image-view {
+  width: 100%;
+}
+
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.image-preview {
+  position: relative;
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 8rpx;
+  overflow: hidden;
+  background: #F5F5F5;
+}
+
+.image-preview image {
+  width: 100%;
+  height: 100%;
+}
+
+.delete-btn {
+  position: absolute;
+  top: -6rpx;
+  right: -6rpx;
+  width: 32rpx;
+  height: 32rpx;
+  background: rgba(0, 0, 0, 0.6);
+  color: #FFFFFF;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20rpx;
+  line-height: 1;
+}
+
+.upload-btn {
+  width: 160rpx;
+  height: 160rpx;
+  background: #F8F8F8;
+  border: 2rpx dashed #DDDDDD;
+  border-radius: 8rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.upload-icon {
+  font-size: 32rpx;
+  margin-bottom: 8rpx;
+  color: #999999;
+}
+
+.upload-text {
+  font-size: 24rpx;
+  color: #999999;
+}
+
+.upload-tip {
+  font-size: 24rpx;
+  color: #999999;
+  margin-top: 12rpx;
+}
+
+.no-images {
+  padding: 40rpx 0;
+  text-align: center;
+  color: #CCCCCC;
+  font-size: 28rpx;
+}
+
+/* 底部安全区域 */
+.bottom-safe {
+  height: 120rpx;
+}
+
+.footer-safe {
+  background: #FFFFFF;
+  border-top: 1rpx solid #E5E5E5;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.footer-content {
+  padding: 24rpx;
+}
+
+.submit-button {
+  width: 100%;
+  height: 88rpx;
+  background: #3BB44A;
+  color: #FFFFFF;
+  border: none;
+  border-radius: 8rpx;
+  font-size: 32rpx;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.submit-button:active {
+  background: #2D8C3C;
+}
+
+.submit-button:disabled,
+.submit-button.loading {
+  background: #CCCCCC;
+}
+
+/* 选择器弹窗样式 */
+.picker-mask {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.5);
+  z-index: 999;
+}
+
+.picker-popup {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  background: #FFFFFF;
+  border-radius: 16rpx 16rpx 0 0;
+  z-index: 1000;
+  padding-bottom: constant(safe-area-inset-bottom);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+.picker-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx;
+  border-bottom: 1rpx solid #E5E5E5;
+}
+
+.picker-cancel, .picker-confirm {
+  font-size: 30rpx;
+  color: #3BB44A;
+}
+
+.picker-title {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #333333;
+}
+
+.picker-content {
+  max-height: 400rpx;
+  overflow-y: auto;
+}
+
+.picker-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx;
+  border-bottom: 1rpx solid #F0F0F0;
+  font-size: 30rpx;
+  color: #333333;
+}
+
+.picker-item.selected {
+  color: #3BB44A;
+}
+
+.picker-item:last-child {
+  border-bottom: none;
+}
+
+.check-mark {
+  color: #3BB44A;
+  font-size: 28rpx;
+}
+
+/* 日期时间选择器样式 */
+.datetime-picker {
+  height: 60vh;
+}
+
+.datetime-picker-content {
+  height: calc(100% - 100rpx);
+  padding: 0;
+}
+
+.datetime-picker-view {
+  width: 100%;
+  height: 100%;
+}
+
+.picker-view-item {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 80rpx;
+  font-size: 32rpx;
+  color: #333333;
+}
+
+.picker-item-text {
+  font-size: 32rpx;
+  color: #333333;
+  line-height: 80rpx;
+  height: 80rpx;
+  text-align: center;
+}
+
+/* 添加加载中样式 */
+.picker-loading {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 30rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.picker-empty {
+  padding: 30rpx 0;
+  text-align: center;
+  color: #CCCCCC;
+  font-size: 28rpx;
+}
+</style> 

+ 207 - 216
pages/activity/index.vue

@@ -113,234 +113,225 @@
   </view>
 </template>
 
-<script>
-import storage from "@/utils/storage.js";
-import { getAgriculturalTasksList, countStatusTypeTasks } from '@/api/services/activity.js';
-import dictMixin from '@/utils/mixins/dictMixin';
-import DictTag from '@/components/common/dict-tag';
-
-export default {
-  components: {
-    DictTag
-  },
-  mixins: [dictMixin],
-  data() {
-    return {
-      // 需要加载的字典类型
-      dictTypeList: ['task_type','task_status'],
-      
-      // 地块数据
-      plotData: {
-        
-      },
-
-      // 待完成任务数
-      pendingTaskCount: 0,
-
-      // 当前筛选状态
-      currentStatus: 'all',
+<script setup>
+import { ref, reactive } from 'vue'
+import storage from "@/utils/storage.js"
+import { onShow, onLoad } from '@dcloudio/uni-app'
+import { getAgriculturalTasksList, countStatusTypeTasks } from '@/api/services/activity.js'
+import { useDict } from '@/utils/composables/useDict'
+import DictTag from '@/components/common/dict-tag'
+
+// 使用字典组合式函数
+const { dictData } = useDict(['task_type', 'task_status'])
+
+// 地块数据
+const plotData = ref({})
+
+// 待完成任务数
+const pendingTaskCount = ref(0)
+
+// 当前筛选状态
+const currentStatus = ref('all')
+
+// 筛选相关数据
+const taskStatus = [
+  { label: '全部', value: 'all' },
+  { label: '待完成', value: 0 },
+  { label: '已完成', value: 1 }
+]
+
+// 任务列表数据
+const taskList = ref([])
+const isLoading = ref(false)
+const isRefreshing = ref(false)
+const noMoreData = ref(false)
+const pageNum = ref(1)
+const pageSize = ref(10)
+
+// 格式化日期
+const formatDate = (dateStr) => {
+  if (!dateStr) return ''
+  
+  // 如果是时间戳,转换为日期对象
+  let date
+  if (typeof dateStr === 'number') {
+    date = new Date(dateStr)
+  } else {
+    date = new Date(dateStr.replace(/-/g, '/'))
+  }
+  
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  
+  return `${year}-${month}-${day}`
+}
 
-      // 筛选相关数据
-      taskStatus: [
-        { label: '全部', value: 'all' },
-        { label: '待完成', value: 0 },
-        { label: '已完成', value: 1 }
-      ],
-      
-      // 任务列表数据
-      taskList: [],
-      isLoading: false,
-      isRefreshing: false,
-      noMoreData: false,
-      pageNum: 1,
-      pageSize: 10,
-    }
-  },
+// 切换任务状态筛选
+const switchStatus = (status) => {
+  if (currentStatus.value === status) return
   
-  methods: {
-    // 格式化日期
-    formatDate(dateStr) {
-      if (!dateStr) return '';
-      
-      // 如果是时间戳,转换为日期对象
-      let date;
-      if (typeof dateStr === 'number') {
-        date = new Date(dateStr);
-      } else {
-        date = new Date(dateStr.replace(/-/g, '/'));
-      }
-      
-      const year = date.getFullYear();
-      const month = String(date.getMonth() + 1).padStart(2, '0');
-      const day = String(date.getDate()).padStart(2, '0');
-      
-      return `${year}-${month}-${day}`;
-    },
-    
-    // 切换任务状态筛选
-    switchStatus(status) {
-      if (this.currentStatus === status) return;
-      
-      this.currentStatus = status;
-      this.pageNum = 1;
-      this.noMoreData = false;
-      this.loadTaskData();
-    },
-    
-    // 加载任务数据
-    loadTaskData() {
-      // this.plotData = JSON.parse(storage.getPlots() || '{}')
-	  this.plotData = JSON.parse(storage.getPlots() || '{}')
-	  const plotId = this.plotData?.id;
-	  // 判断地块是否存在且有 id
-	  if (plotId == undefined) {
-	    uni.showToast({
-	      title: '请选择地块!',
-	      icon: 'none'
-	    });
-	    return
-	  }
-	  console.log("this.plotData",this.plotData);
-      this.isLoading = true;
-      
-      // 构建查询参数
-      const params = {
-        pageNum: this.pageNum,
-        pageSize: this.pageSize,
-        plotId: plotId
-      };
-      
-      // 如果不是查询全部,添加状态过滤条件
-      if (this.currentStatus !== 'all') {
-        params.taskStatus = this.currentStatus;
-      }
-      
-      getAgriculturalTasksList(params).then(res => {
-        if (res.data.code === 200) {
-          const { rows, total } = res.data;
-          
-          // 处理返回的数据
-          const tasks = rows.map(item => {
-            // 确保每个任务都有remarkExpanded属性
-            item.remarkExpanded = false;
-            return item;
-          });
-          
-          if (this.pageNum === 1) {
-            this.taskList = tasks;
-          } else {
-            this.taskList = [...this.taskList, ...tasks];
-          }
-          
-          // 判断是否还有更多数据
-          this.noMoreData = this.taskList.length >= total;
-          
-          // 统计待完成任务数量
-          this.countPendingTasks();
-        } else {
-          uni.showToast({
-            title: res.data.msg || '获取任务列表失败',
-            icon: 'none'
-          });
-        }
-      }).catch(err => {
-        console.error('获取任务列表失败:', err);
-        uni.showToast({
-          title: '获取任务列表失败',
-          icon: 'none'
-        });
-      }).finally(() => {
-        this.isLoading = false;
-        this.isRefreshing = false;
-      });
-    },
-    
-    // 统计待完成任务数量
-    countPendingTasks() {
-      // 如果当前已经是按状态筛选,直接使用当前列表长度
-      if (this.currentStatus === 'pending') {
-        this.pendingTaskCount = this.taskList.length;
-        return;
-      }
+  currentStatus.value = status
+  pageNum.value = 1
+  noMoreData.value = false
+  loadTaskData()
+}
+
+// 加载任务数据
+const loadTaskData = () => {
+  plotData.value = JSON.parse(storage.getPlots() || '{}')
+  const plotId = plotData.value?.id
+  // 判断地块是否存在且有 id
+  if (plotId == undefined) {
+    uni.showToast({
+      title: '请选择地块!',
+      icon: 'none'
+    })
+    return
+  }
+  console.log("plotData.value", plotData.value)
+  isLoading.value = true
+  
+  // 构建查询参数
+  const params = {
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+    plotId: plotId
+  }
+  
+  // 如果不是查询全部,添加状态过滤条件
+  if (currentStatus.value !== 'all') {
+    params.taskStatus = currentStatus.value
+  }
+  
+  getAgriculturalTasksList(params).then(res => {
+    if (res.data.code === 200) {
+      const { rows, total } = res.data
       
-      // 否则,请求获取待完成任务数量
-      const params = {
-        plotId: this.plotData.id,
-        taskStatus: 0
-      };
+      // 处理返回的数据
+      const tasks = rows.map(item => {
+        // 确保每个任务都有remarkExpanded属性
+        item.remarkExpanded = false
+        return item
+      })
       
-      countStatusTypeTasks(params).then(res => {
-		  console.log("nongsh",res);
-        if (res.data.code === 200) {
-          this.pendingTaskCount = res.data.data || 0;
-        }
-      });
-    },
-
-    // 下拉刷新
-    refreshData(plotId) {
-      this.isRefreshing = true;
-      this.pageNum = 1;
-      this.noMoreData = false;
-      this.loadTaskData(plotId);
-    },
-
-    // 上拉加载更多
-    loadMore() {
-      if (!this.isLoading && !this.noMoreData) {
-        this.pageNum++;
-        this.loadTaskData();
-      }
-    },
-
-    // 查看任务详情
-    viewTaskDetail(task) {
-      // 根据任务状态决定跳转到不同模式的页面
-      let mode = '';
-      if (task.taskStatus === '0') {
-        // 待完成任务跳转到编辑页面
-        mode = 'edit';
-      } else if (task.taskStatus === '1') {
-        // 已完成任务跳转到查看页面
-        mode = 'view';
+      if (pageNum.value === 1) {
+        taskList.value = tasks
+      } else {
+        taskList.value = [...taskList.value, ...tasks]
       }
-      console.log("this.plotData",this.plotData);
-      // 使用简化的URL参数传递
-      const url = `/pages/activity/activity-detail?id=${task.id}&mode=${mode}&fieldName=${encodeURIComponent(this.plotData.name)}&growCrops=${encodeURIComponent(this.plotData.growCrops)}&farmId=${encodeURIComponent(this.plotData.farmId)}&manager=${encodeURIComponent(this.plotData.managerName)}&plotId=${this.plotData.id}`;
       
-      uni.navigateTo({
-        url: url
-      });
-    },
-
-    // 创建新任务
-    createNewTask() {
-      // 使用简化的URL参数传递
-      const url = `/pages/activity/activity-detail?id=new&mode=create&fieldName=${encodeURIComponent(this.plotData.name)}&growCrops=${encodeURIComponent(this.plotData.growCrops)}&farmId=${encodeURIComponent(this.plotData.farmId)}&manager=${encodeURIComponent(this.plotData.managerName)}&plotId=${this.plotData.id}`;
+      // 判断是否还有更多数据
+      noMoreData.value = taskList.value.length >= total
       
-      uni.navigateTo({
-        url: url
-      });
-    },
-    
-    // 切换备注展开/收起状态
-    toggleRemark(index) {
-      if (this.taskList[index].remark && this.taskList[index].remark.length > 50) {
-        this.$set(this.taskList[index], 'remarkExpanded', !this.taskList[index].remarkExpanded);
-      }
+      // 统计待完成任务数量
+      countPendingTasks()
+    } else {
+      uni.showToast({
+        title: res.data.msg || '获取任务列表失败',
+        icon: 'none'
+      })
     }
-  },
+  }).catch(err => {
+    console.error('获取任务列表失败:', err)
+    uni.showToast({
+      title: '获取任务列表失败',
+      icon: 'none'
+    })
+  }).finally(() => {
+    isLoading.value = false
+    isRefreshing.value = false
+  })
+}
+
+// 统计待完成任务数量
+const countPendingTasks = () => {
+  // 如果当前已经是按状态筛选,直接使用当前列表长度
+  if (currentStatus.value === 'pending') {
+    pendingTaskCount.value = taskList.value.length
+    return
+  }
   
-  // 页面加载时获取数据
-  onLoad() {
-	this.loadTaskData();
-  },
+  // 否则,请求获取待完成任务数量
+  const params = {
+    plotId: plotData.value.id,
+    taskStatus: 0
+  }
   
-  // 页面显示时刷新数据
-  onShow() {
-	this.refreshData()
+  countStatusTypeTasks(params).then(res => {
+    console.log("nongsh", res)
+    if (res.data.code === 200) {
+      pendingTaskCount.value = res.data.data || 0
+    }
+  })
+}
+
+// 下拉刷新
+const refreshData = (plotId) => {
+  isRefreshing.value = true
+  pageNum.value = 1
+  noMoreData.value = false
+  loadTaskData(plotId)
+}
+
+// 上拉加载更多
+const loadMore = () => {
+  if (!isLoading.value && !noMoreData.value) {
+    pageNum.value++
+    loadTaskData()
   }
 }
+
+// 查看任务详情
+const viewTaskDetail = (task) => {
+  // 根据任务状态决定跳转到不同模式的页面
+  let mode = ''
+  if (task.taskStatus === '0') {
+    // 待完成任务跳转到编辑页面
+    mode = 'edit'
+  } else if (task.taskStatus === '1') {
+    // 已完成任务跳转到查看页面
+    mode = 'view'
+  }
+  console.log("plotData.value", plotData.value)
+  // 使用简化的URL参数传递
+  const url = `/pages/activity/activity-detail?id=${task.id}&mode=${mode}&fieldName=${encodeURIComponent(plotData.value.name)}&growCrops=${encodeURIComponent(plotData.value.growCrops)}&farmId=${encodeURIComponent(plotData.value.farmId)}&manager=${encodeURIComponent(plotData.value.managerName)}&plotId=${plotData.value.id}`
+  
+  uni.navigateTo({
+    url: url
+  })
+}
+
+// 创建新任务
+const createNewTask = () => {
+  // 使用简化的URL参数传递
+  const url = `/pages/activity/activity-detail?id=new&mode=create&fieldName=${encodeURIComponent(plotData.value.name)}&growCrops=${encodeURIComponent(plotData.value.growCrops)}&farmId=${encodeURIComponent(plotData.value.farmId)}&manager=${encodeURIComponent(plotData.value.managerName)}&plotId=${plotData.value.id}`
+  
+  uni.navigateTo({
+    url: url
+  })
+}
+
+// 切换备注展开/收起状态
+const toggleRemark = (index) => {
+  if (taskList.value[index].remark && taskList.value[index].remark.length > 50) {
+    taskList.value[index].remarkExpanded = !taskList.value[index].remarkExpanded
+  }
+}
+
+// 页面加载时获取数据
+ onLoad(()=>{
+    loadTaskData()
+ })
+  
+
+
+// 页面显示时刷新数据
+onShow(()=>{
+  refreshData()
+})
+
+
 </script>
 
 <style scoped>

+ 96 - 98
pages/chart/index.vue

@@ -13,104 +13,102 @@
   </view>
 </template>
 
-<script>
-	import {
-		chartStream
-	} from '@/api/services/chart.js';
-export default {
-  data() {
-    return {
-      inputText: '',
-      messages: [],
-      loading: false,
-      scrollIntoView: ''
-    };
-  },
-  methods: {
-    sendMessage() {
-      const content = this.inputText.trim();
-      if (!content || this.loading) return;
-
-      this.messages.push({ role: 'user', content });
-      this.inputText = '';
-      this.scrollToBottom();
-
-      const aiMsg = { role: 'ai', content: '' };
-      this.messages.push(aiMsg);
-      this.loading = true;
-      this.scrollToBottom();
-
-      const requestBody = {
-        query: content,
-        user: 'test_user_123' // 实际项目中应为真实用户ID
-      };
-
-      uni.request({
-        url: 'http://localhost:9203/dify/chat/stream', // ⚠️ 换成你自己的后端
-        method: 'POST',
-        header: {
-          'Content-Type': 'application/json'
-        },
-        data: requestBody,
-        success: (res) => {
-          const reader = res.data.getReader?.();
-          const decoder = new TextDecoder();
-          let buffer = '';
-
-          const readStream = () => {
-            reader.read().then(({ done, value }) => {
-              if (done) {
-                this.loading = false;
-                return;
-              }
-
-              buffer += decoder.decode(value, { stream: true });
-              const events = buffer.split('\n\n');
-              buffer = events.pop(); // 保留未完成部分
-
-              events.forEach(evt => {
-                if (!evt.trim()) return;
-                const lines = evt.split('\n');
-                let eventData = '';
-
-                lines.forEach(line => {
-                  if (line.startsWith('data:')) {
-                    eventData = line.replace(/^data:\s*/, '');
-                  }
-                });
-
-                try {
-                  const json = JSON.parse(eventData);
-                  if (json.event === 'message') {
-                    aiMsg.content += json.answer || '';
-                    this.scrollToBottom();
-                  } else if (json.event === 'message_end') {
-                    this.loading = false;
-                  }
-                } catch (e) {
-                  console.warn('解析流数据失败:', e);
-                }
-              });
-
-              readStream();
-            });
-          };
-
-          readStream();
-        },
-        fail: (err) => {
-          this.loading = false;
-          aiMsg.content = '[请求失败: ' + err.errMsg + ']';
-        }
-      });
-    },
-    scrollToBottom() {
-      this.$nextTick(() => {
-        this.scrollIntoView = 'msg-' + (this.messages.length - 1);
-      });
-    }
-  }
-};
+<script setup>
+import { ref, nextTick } from 'vue'
+import {
+	chartStream
+} from '@/api/services/chart.js'
+
+// 响应式数据
+const inputText = ref('')
+const messages = ref([])
+const loading = ref(false)
+const scrollIntoView = ref('')
+
+// 发送消息
+const sendMessage = () => {
+	const content = inputText.value.trim()
+	if (!content || loading.value) return
+
+	messages.value.push({ role: 'user', content })
+	inputText.value = ''
+	scrollToBottom()
+
+	const aiMsg = { role: 'ai', content: '' }
+	messages.value.push(aiMsg)
+	loading.value = true
+	scrollToBottom()
+
+	const requestBody = {
+		query: content,
+		user: 'test_user_123' // 实际项目中应为真实用户ID
+	}
+
+	uni.request({
+		url: 'http://localhost:9203/dify/chat/stream', // ⚠️ 换成你自己的后端
+		method: 'POST',
+		header: {
+			'Content-Type': 'application/json'
+		},
+		data: requestBody,
+		success: (res) => {
+			const reader = res.data.getReader?.()
+			const decoder = new TextDecoder()
+			let buffer = ''
+
+			const readStream = () => {
+				reader.read().then(({ done, value }) => {
+					if (done) {
+						loading.value = false
+						return
+					}
+
+					buffer += decoder.decode(value, { stream: true })
+					const events = buffer.split('\n\n')
+					buffer = events.pop() // 保留未完成部分
+
+					events.forEach(evt => {
+						if (!evt.trim()) return
+						const lines = evt.split('\n')
+						let eventData = ''
+
+						lines.forEach(line => {
+							if (line.startsWith('data:')) {
+								eventData = line.replace(/^data:\s*/, '')
+							}
+						})
+
+						try {
+							const json = JSON.parse(eventData)
+							if (json.event === 'message') {
+								aiMsg.content += json.answer || ''
+								scrollToBottom()
+							} else if (json.event === 'message_end') {
+								loading.value = false
+							}
+						} catch (e) {
+							console.warn('解析流数据失败:', e)
+						}
+					})
+
+					readStream()
+				})
+			}
+
+			readStream()
+		},
+		fail: (err) => {
+			loading.value = false
+			aiMsg.content = '[请求失败: ' + err.errMsg + ']'
+		}
+	})
+}
+
+const scrollToBottom = () => {
+	nextTick(() => {
+		scrollIntoView.value = 'msg-' + (messages.value.length - 1)
+	})
+}
 </script>
 
 <style scoped>

+ 454 - 413
pages/dashboard/index.vue

@@ -5,11 +5,17 @@
 			<view class="user-info">
 				<text class="greeting">您好,{{ userData.nickname }}</text>
 				<view class="plot-info">
-					<text class="plot-label">当前地块</text>
+					<text class="plot-label">当前地块:</text>
 					<text class="plot-name">{{ userData.selectedPlot }}</text>
 					<view class="switch-plot-btn" v-if="isLogin">
-						<u-picker :show="show" :columns="columns" :defaultIndex="defaultIndex" @cancel="show = false"
-							@confirm="onConfirm"></u-picker>
+						<u-picker 
+							:show="show" 
+							:columns="columns" 
+							:defaultIndex="defaultIndex" 
+							@cancel="handleCancel"
+							@confirm="onConfirm"
+							keyName="text"
+						></u-picker>
 						<text @click="handleSwitchPlot">切换</text>
 					</view>
 
@@ -247,429 +253,464 @@
 	</view>
 </template>
 
-<script>
-	import storage from "@/utils/storage.js";
-	import {
-		listFieldName
-	} from "@/api/services/field.js"
-	export default {
-		data() {
-			return {
-				isLogin:false,
-				show: false,
-				columns: [
-					[]
-				],
-				defaultIndex: [0],
-				// 用户数据
-				userData: {
-					nickname: '游客',
-					selectedPlot: '未选择地块',
-					avatar: '/static/images/user-avatar.png',
-				},
-
-				// 农场数据
-				farmData: {
-					plotCount: 5,
-					deviceCount: 12,
-					deviceOnlineRate: 85,
-					taskCompletionRate: 76,
-					recentActivities: [{
-							title: '稻田水稻施肥',
-							date: '2023-05-15',
-							status: 'pending',
-							id: 1,
-							executor: '李四'
-						},
-						{
-							title: '南地块除草',
-							date: '2023-05-12',
-							status: 'completed',
-							id: 2,
-							executor: '王五'
-						},
-						{
-							title: '西北区域杀虫',
-							date: '2023-05-08',
-							status: 'completed',
-							id: 3,
-							executor: '张三'
-						}
-					]
-				},
-
-				// 天气数据
-				weatherData: {
-					temperature: 28,
-					description: '晴朗',
-					humidity: 65,
-					windLevel: 3,
-					rainfall: 0,
-					advice: '今日适宜进行春玉米防虫作业,注意水分管理。'
-				},
-
-				// 作物数据
-				crops: [{
-						name: '水稻',
-						area: 48,
-						progress: 75,
-						icon: '🌾',
-						bgColor: '#4CAF50'
-					},
-					{
-						name: '小麦',
-						area: 36,
-						progress: 60,
-						icon: '🌿',
-						bgColor: '#66BB6A'
-					},
-					{
-						name: '玉米',
-						area: 32,
-						progress: 85,
-						icon: '🌽',
-						bgColor: '#43A047'
-					},
-					{
-						name: '大豆',
-						area: 12,
-						progress: 40,
-						icon: '🫘',
-						bgColor: '#388E3C'
-					}
-				],
-
-				// 监控设备指标
-				deviceMetrics: [{
-						name: '在线设备',
-						value: '28',
-						icon: 'wifi',
-						gradient: 'linear-gradient(135deg, #26A69A, #00796B)',
-						trend: {
-							type: 'up',
-							value: '5.2%'
-						}
-					},
-					{
-						name: '告警设备',
-						value: '3',
-						icon: 'error-circle',
-						gradient: 'linear-gradient(135deg, #FF7043, #E64A19)',
-						trend: {
-							type: 'down',
-							value: '2.1%'
-						}
-					},
-					{
-						name: '离线设备',
-						value: '5',
-						icon: 'close-circle',
-						gradient: 'linear-gradient(135deg, #78909C, #455A64)',
-						trend: {
-							type: 'down',
-							value: '1.8%'
-						}
-					},
-					{
-						name: '数据稳定性',
-						value: '96.3%',
-						icon: 'checkmark-circle',
-						gradient: 'linear-gradient(135deg, #66BB6A, #388E3C)',
-						trend: {
-							type: 'up',
-							value: '0.5%'
-						}
-					}
-				],
-
-				// 农业机械指标
-				machineryMetrics: [{
-						name: '今日运行时长',
-						value: '36.5',
-						unit: '小时',
-						icon: 'clock',
-						gradient: 'linear-gradient(135deg, #42A5F5, #1976D2)'
-					},
-					{
-						name: '今日作业地块',
-						value: '8',
-						unit: '块',
-						icon: 'map',
-						gradient: 'linear-gradient(135deg, #66BB6A, #388E3C)'
-					},
-					{
-						name: '今日执行任务',
-						value: '12',
-						unit: '个',
-						icon: 'calendar',
-						gradient: 'linear-gradient(135deg, #FFA726, #F57C00)'
-					},
-					{
-						name: '使用率',
-						value: '78.2%',
-						icon: 'star',
-						gradient: 'linear-gradient(135deg, #5C6BC0, #3949AB)',
-						trend: {
-							type: 'up',
-							value: '3.7%'
-						}
-					}
-				],
-
-				// 当前选择的周期
-				currentPeriod: 'month',
-
-				// 警报摘要数据
-				alertSummaries: [{
-						title: '设备离线',
-						value: '3 台',
-						iconSrc: '/static/icons/offline.png',
-						description: '最长离线时长:26 小时',
-						type: 'device-offline'
-					},
-					{
-						title: '虫害预警',
-						value: '稻飞虱|小地老虎',
-						iconSrc: '/static/icons/Pest_Alert.png',
-						description: '预警等级:中',
-						type: 'pest-warning'
-					},
-					{
-						title: '气象预警',
-						value: '强风(8级)',
-						iconSrc: '/static/icons/weather_risk.png',
-						description: '预计:未来 12 小时内',
-						type: 'weather-risk'
-					},
-					{
-						title: '作业延迟',
-						value: '5 项',
-						iconSrc: '/static/icons/task_delay.png',
-						description: '最长延迟:3 天',
-						type: 'task-delay'
-					}
-				],
-
-				// 农场绩效图表数据 - 月度数据
-				monthlyData: {
-					months: ['1月', '2月', '3月', '4月', '5月', '6月'],
-					thisYearValues: [2.8, 3.4, 2.9, 3.6, 3.8, 3.2],
-					lastYearValues: [2.5, 2.8, 2.4, 3.0, 3.2, 2.7]
-				},
-
-				// 季度数据
-				quarterlyData: {
-					months: ['Q1', 'Q2', 'Q3', 'Q4'],
-					thisYearValues: [3.2, 3.7, 4.0, 3.5],
-					lastYearValues: [2.7, 3.3, 3.6, 3.0]
-				},
-
-				// 年度数据
-				yearlyData: {
-					months: ['2019', '2020', '2021', '2022', '2023'],
-					thisYearValues: [2.2, 2.5, 3.0, 3.5, 3.8],
-					lastYearValues: [2.0, 2.3, 2.7, 3.2, 3.4]
-				}
-			};
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import storage from "@/utils/storage.js"
+import { listFieldName } from "@/api/services/field.js"
+
+// 响应式数�?
+const isLogin = ref(false)
+const show = ref(false)
+const columns = ref([[]])
+const defaultIndex = ref([0])
+
+// 用户数据
+const userData = reactive({
+nickname: '游客',
+selectedPlot: '未选择地块',
+avatar: '/static/images/user-avatar.png',
+userId: null
+})
+
+// 农场数据
+const farmData = reactive({
+	plotCount: 5,
+	deviceCount: 12,
+	deviceOnlineRate: 85,
+	taskCompletionRate: 76,
+	recentActivities: [
+		{
+			title: '稻田水稻施肥',
+			date: '2023-05-15',
+			status: 'pending',
+			id: 1,
+			executor: '李四'
 		},
-
-		computed: {
-			// 核心统计数据
-			coreStats() {
-				return [{
-						label: '地块',
-						value: this.farmData.plotCount,
-						icon: 'map',
-					},
-					{
-						label: '设备',
-						value: this.farmData.deviceCount,
-						icon: 'setting',
-					},
-					{
-						label: '设备在线',
-						value: this.farmData.deviceOnlineRate + '%',
-						icon: 'wifi',
-					},
-					{
-						label: '任务完成',
-						value: this.farmData.taskCompletionRate + '%',
-						icon: 'checkmark-circle',
-					}
-				];
-			},
-
-			// 根据当前周期计算要显示的数据
-			farmPerformanceData() {
-				if (this.currentPeriod === 'month') {
-					return this.monthlyData;
-				} else if (this.currentPeriod === 'quarter') {
-					return this.quarterlyData;
-				} else {
-					return this.yearlyData;
-				}
-			}
+		{
+			title: '南地块除草',
+			date: '2023-05-12',
+			status: 'completed',
+			id: 2,
+			executor: '王五'
 		},
+		{
+			title: '西北区域杀虫',
+			date: '2023-05-08',
+			status: 'completed',
+			id: 3,
+			executor: '张三'
+		}
+	]
+})
+
+// 天气数据
+const weatherData = reactive({
+	temperature: 28,
+	description: '晴朗',
+	humidity: 65,
+	windLevel: 3,
+	rainfall: 0,
+	advice: '今日适宜进行春玉米防虫作业,注意水分管理。'
+})
+
+// 作物数据
+const crops = ref([
+	{
+		name: '水稻',
+		area: 48,
+		progress: 75,
+		icon: '🌾',
+		bgColor: '#4CAF50'
+	},
+	{
+		name: '小麦',
+		area: 36,
+		progress: 60,
+		icon: '🌿',
+		bgColor: '#66BB6A'
+	},
+	{
+		name: '玉米',
+		area: 32,
+		progress: 85,
+		icon: '🌽',
+		bgColor: '#43A047'
+	},
+	{
+		name: '大豆',
+		area: 12,
+		progress: 40,
+		icon: '🫘',
+		bgColor: '#388E3C'
+	}
+])
+
+// 监控设备指标
+const deviceMetrics = ref([
+	{
+		name: '在线设备',
+		value: '28',
+		icon: 'wifi',
+		gradient: 'linear-gradient(135deg, #26A69A, #00796B)',
+		trend: {
+			type: 'up',
+			value: '5.2%'
+		}
+	},
+	{
+		name: '告警设备',
+		value: '3',
+		icon: 'error-circle',
+		gradient: 'linear-gradient(135deg, #FF7043, #E64A19)',
+		trend: {
+			type: 'down',
+			value: '2.1%'
+		}
+	},
+	{
+		name: '离线设备',
+		value: '5',
+		icon: 'close-circle',
+		gradient: 'linear-gradient(135deg, #78909C, #455A64)',
+		trend: {
+			type: 'down',
+			value: '1.8%'
+		}
+	},
+	{
+		name: '数据稳定性',
+		value: '96.3%',
+		icon: 'checkmark-circle',
+		gradient: 'linear-gradient(135deg, #66BB6A, #388E3C)',
+		trend: {
+			type: 'up',
+			value: '0.5%'
+		}
+	}
+])
+
+// 农业机械指标
+const machineryMetrics = ref([
+	{
+		name: '今日运行时长',
+		value: '36.5',
+		unit: '小时',
+		icon: 'clock',
+		gradient: 'linear-gradient(135deg, #42A5F5, #1976D2)'
+	},
+	{
+		name: '今日作业地块',
+		value: '8',
+		unit: '块',
+		icon: 'map',
+		gradient: 'linear-gradient(135deg, #66BB6A, #388E3C)'
+	},
+	{
+		name: '今日执行任务',
+		value: '12',
+		unit: '个',
+		icon: 'calendar',
+		gradient: 'linear-gradient(135deg, #FFA726, #F57C00)'
+	},
+	{
+		name: '使用率',
+		value: '78.2%',
+		icon: 'star',
+		gradient: 'linear-gradient(135deg, #5C6BC0, #3949AB)',
+		trend: {
+			type: 'up',
+			value: '3.7%'
+		}
+	}
+])
+
+// 当前选择的周�?
+const currentPeriod = ref('month')
+
+// 警报摘要数据
+const alertSummaries = ref([
+	{
+		title: '设备离线',
+		value: '3 台',
+		iconSrc: '/static/icons/offline.png',
+		description: '最长离线时长:26 小时',
+		type: 'device-offline'
+	},
+	{
+		title: '虫害预警',
+		value: '稻飞虱|小地老虎',
+		iconSrc: '/static/icons/Pest_Alert.png',
+		description: '预警等级:中',
+		type: 'pest-warning'
+	},
+	{
+		title: '气象预警',
+		value: '强风(8级)',
+		iconSrc: '/static/icons/weather_risk.png',
+		description: '预计:未来 12 小时内',
+		type: 'weather-risk'
+	},
+	{
+		title: '作业延迟',
+		value: '5 项',
+		iconSrc: '/static/icons/task_delay.png',
+		description: '最长延迟:3 天',
+		type: 'task-delay'
+	}
+])
+
+// 农场绩效图表数据 - 月度数据
+const monthlyData = reactive({
+	months: ['1月', '2月', '3月', '4月', '5月', '6月'],
+	thisYearValues: [2.8, 3.4, 2.9, 3.6, 3.8, 3.2],
+	lastYearValues: [2.5, 2.8, 2.4, 3.0, 3.2, 2.7]
+})
+
+// 季度数据
+const quarterlyData = reactive({
+	months: ['Q1', 'Q2', 'Q3', 'Q4'],
+	thisYearValues: [3.2, 3.7, 4.0, 3.5],
+	lastYearValues: [2.7, 3.3, 3.6, 3.0]
+})
+
+// 年度数据
+const yearlyData = reactive({
+	months: ['2019', '2020', '2021', '2022', '2023'],
+	thisYearValues: [2.2, 2.5, 3.0, 3.5, 3.8],
+	lastYearValues: [2.0, 2.3, 2.7, 3.2, 3.4]
+})
+
+// 计算属�?- 核心统计数据
+const coreStats = computed(() => {
+	return [
+		{
+			label: '地块',
+			value: farmData.plotCount,
+			icon: 'map',
+		},
+		{
+			label: '设备',
+			value: farmData.deviceCount,
+			icon: 'setting',
+		},
+		{
+			label: '设备在线',
+			value: farmData.deviceOnlineRate + '%',
+			icon: 'wifi',
+		},
+		{
+			label: '任务完成',
+			value: farmData.taskCompletionRate + '%',
+			icon: 'checkmark-circle',
+		}
+	]
+})
+
+// 计算属�?- 根据当前周期计算要显示的数据
+const farmPerformanceData = computed(() => {
+	if (currentPeriod.value === 'month') {
+		return monthlyData
+	} else if (currentPeriod.value === 'quarter') {
+		return quarterlyData
+	} else {
+		return yearlyData
+	}
+})
 
-		methods: {
-			loadPhots(userId) {
-				console.log("加载用户字段", userId);
-
-				listFieldName(userId)
-					.then(res => {
-						const {
-							data
-						} = res || {};
-
-						if (data?.code !== 200 || !Array.isArray(data.data)) {
-							console.warn("接口返回异常或数据格式不正确", res);
-							return;
-						}
+// 方法定义
+const loadPhots = (userId) => {
+	console.log("加载用户字段", userId)
 
-						const fieldList = data.data;
-						console.log("fieldList:",fieldList);
-						const fields = [];
-						fieldList.forEach(item => {
-							const fieldName = item?.fieldName;
-							if (fieldName && !fields.includes(fieldName)) {
-								fields.push({
-									text: fieldName,
-									value: item.id,
-									growCrops: item.growCrops,
-									managerName: item.managerName,
-									size: item.size,
-									farmId: item.farmId
-								});
-							}
-						});
+	listFieldName(userId)
+		.then(res => {
+			const { data } = res || {}
 
-						this.columns = [fields]; // 符合 u-picker 的二维数组格式
-						storage.setCurrentUserPlotsList(fields)
+			if (data?.code !== 200 || !Array.isArray(data.data)) {
+				console.warn("接口返回异常或数据格式不正确", res)
+				return
+			}
 
-						console.log("字段加载完成:", this.columns);
-						
-						// 字段加载完成后立即设置默认索引
-						this.setDefaultSelectedPlot();
+			const fieldList = data.data
+			console.log("fieldList:", fieldList)
+			const fields = []
+			fieldList.forEach(item => {
+				const fieldName = item?.fieldName
+				if (fieldName && !fields.includes(fieldName)) {
+					fields.push({
+						text: fieldName,
+						value: item.id,
+						growCrops: item.growCrops,
+						managerName: item.managerName,
+						size: item.size,
+						farmId: item.farmId
 					})
-					.catch(err => {
-						console.error("加载字段失败", err);
-					});
-			},
-
-			// 初始化地块列表
-			initPlotColumns() {
-				const cached = storage.getCurrentUserPlotsList();
-				if (Array.isArray(cached) && cached.length > 0) {
-					this.columns = [cached]; // 结构上是二维数组
-					console.log("从缓存加载字段:", this.columns);
-				} else {
-					this.loadPhots(this.userData.userid); // 异步方法内部要处理赋值 columns
 				}
-			},
-
-			// 设置默认选中项
-			setDefaultSelectedPlot() {
-				try {
-					const currentPlots = JSON.parse(storage.getPlots() || '{}');
-					console.log("设置默认地块", currentPlots);
-					
-					if (currentPlots?.name) {
-						this.userData.selectedPlot = currentPlots.name;
-
-						// 防止 this.columns 未定义或格式不正确
-						if (Array.isArray(this.columns) && this.columns.length > 0 && Array.isArray(this.columns[0])) {
-							const plots = this.columns[0];
-							const index = plots.findIndex(item => item.text === currentPlots.name);
-
-							if (index !== -1) {
-								this.defaultIndex = [index];
-								console.log("设置默认选中地块索引:", index, "defaultIndex:", this.defaultIndex);
-							} else {
-								console.warn("未找到匹配的地块:", currentPlots.name);
-								this.defaultIndex = [0]; // 默认选中第一项
-							}
-						} else {
-							console.warn("columns数据格式不正确:", this.columns);
-							this.defaultIndex = [0]; // 默认选中第一项
-						}
-					} else {
-						console.warn("未设置默认地块或地块名为空");
-						this.defaultIndex = [0]; // 默认选中第一项
-					}
-				} catch (error) {
-					console.error("设置默认地块时出错:", error);
-					this.defaultIndex = [0]; // 出错时默认选中第一项
-				}
-			},
-			onConfirm(e) {
-				console.log('选择了:', e);
-				this.userData.selectedPlot = e.value[0].text
-				let obj = {id:e.value[0].value,name:e.value[0].text,growCrops:e.value[0].growCrops,managerName:e.value[0].managerName,size:e.value[0].size,farmId: e.value[0].farmId}
-				storage.setPlots(JSON.stringify(obj))
-				this.show = false;
-			},
-			// 处理切换地块
-			handleSwitchPlot() {
-				// 在显示picker前重新设置defaultIndex
-				this.setDefaultSelectedPlot();
-				this.show = true;
-			},
-
-			// 导航到个人资料
-			navigateToProfile() {
-				uni.navigateTo({
-					url: '/pages/user/index'
-				});
-			},
-
-			// 导航到活动详情
-			navigateToActivity(activity) {
-				uni.navigateTo({
-					url: `/pages/activity/detail?id=${activity.id}`
-				});
-			},
-
-			// 导航到警报详情
-			navigateToAlertDetail(type) {
-				// 实现导航到警报详情页的逻辑
-				console.log(`Navigating to alert detail for type: ${type}`);
-			},
-
-			// 切换周期
-			changePeriod(period) {
-				this.currentPeriod = period;
-			}
-		},
-		onShow() {
-			const userInfo = storage.getUserInfo()
-			console.log("userInfo", userInfo);
-			this.userData.nickname = userInfo.username || '未登录'
-			this.userData.avatar = userInfo.avatar || '/static/icons/user_icon.png'
-			this.userData.userId = userInfo.userid
+			})
+
+			columns.value = [fields] // 符合 u-picker 的二维数组格�?
+			storage.setCurrentUserPlotsList(fields)
+
+			console.log("字段加载完成:", columns.value)
 			
-			if(storage.getHasLogin()){
-				this.isLogin = true;
-				// 加载当前登录用户所属所有地块信息
-				const cached = storage.getCurrentUserPlotsList();
-				if (Array.isArray(cached) && cached.length > 0) {
-					// 如果有缓存,直接用缓存构造 columns
-					this.columns = [cached];
-					console.log("从缓存加载字段:", this.columns);
-					// 有缓存时,在columns设置后立即设置defaultIndex
-					this.setDefaultSelectedPlot();
+			// 字段加载完成后立即设置默认索�?
+			setDefaultSelectedPlot()
+		})
+		.catch(err => {
+			console.error("加载字段失败", err)
+		})
+}
+
+// 初始化地块列�?
+const initPlotColumns = () => {
+	const cached = storage.getCurrentUserPlotsList()
+	if (Array.isArray(cached) && cached.length > 0) {
+		columns.value = [cached] // 结构上是二维数组
+		console.log("从缓存加载字�?", columns.value)
+	} else {
+		loadPhots(userData.userid) // 异步方法内部要处理赋�?columns
+	}
+}
+
+// 设置默认选中�?
+const setDefaultSelectedPlot = () => {
+	try {
+		const currentPlots = JSON.parse(storage.getPlots() || '{}')
+		console.log("设置默认地块", currentPlots)
+		
+		if (currentPlots?.name) {
+			userData.selectedPlot = currentPlots.name
+
+			// 防止 columns 未定义或格式不正�?
+			if (Array.isArray(columns.value) && columns.value.length > 0 && Array.isArray(columns.value[0])) {
+				const plots = columns.value[0]
+				const index = plots.findIndex(item => item.text === currentPlots.name)
+
+				if (index !== -1) {
+					defaultIndex.value = [index]
+					console.log("设置默认选中地块索引:", index, "defaultIndex:", defaultIndex.value)
 				} else {
-					// 无缓存时请求接口,在接口完成后会调用setDefaultSelectedPlot
-					this.loadPhots(this.userData.userId);
+					console.warn("未找到匹配的地块:", currentPlots.name)
+					defaultIndex.value = [0] // 默认选中第一�?
 				}
-				console.log("columns", this.columns);
+			} else {
+				console.warn("columns数据格式不正�?", columns.value)
+				defaultIndex.value = [0] // 默认选中第一�?
 			}
-
-		},
-
-		mounted() {
-			
+		} else {
+			console.warn("未设置默认地块或地块名为空");
+			this.defaultIndex = [0]; // 默认选中第一项
+		}
+	} catch (error) {
+		console.error("设置默认地块时出�?", error)
+		defaultIndex.value = [0] // 出错时默认选中第一�?
+	}
+}
+
+const onConfirm = (e) => {
+	console.log('选择了:', e)
+	userData.selectedPlot = e.value[0].text
+	let obj = {
+		id: e.value[0].value,
+		name: e.value[0].text,
+		growCrops: e.value[0].growCrops,
+		managerName: e.value[0].managerName,
+		size: e.value[0].size,
+		farmId: e.value[0].farmId
+	}
+	storage.setPlots(JSON.stringify(obj))
+	show.value = false
+}
+
+// 处理取消
+const handleCancel = () => {
+	console.log('取消选择')
+	show.value = false
+}
+
+// 处理切换地块
+const handleSwitchPlot = () => {
+	// 在显示picker前重新设置defaultIndex
+	setDefaultSelectedPlot()
+	show.value = true
+}
+
+// 导航到个人资�?
+const navigateToProfile = () => {
+	uni.navigateTo({
+		url: '/pages/user/index'
+	})
+}
+
+// 导航到活动详�?
+const navigateToActivity = (activity) => {
+	uni.navigateTo({
+		url: `/pages/activity/detail?id=${activity.id}`
+	})
+}
+
+// 导航到警报详�?
+const navigateToAlertDetail = (type) => {
+	// 实现导航到警报详情页的逻辑
+	console.log(`Navigating to alert detail for type: ${type}`)
+}
+
+// 切换周期
+const changePeriod = (period) => {
+	currentPeriod.value = period
+}
+
+// uni-app 生命周期 - onShow
+onShow(() => {
+	const userInfo = storage.getUserInfo()
+	console.log("userInfo", userInfo)
+	userData.nickname = userInfo.username || '未登录'
+	userData.avatar = userInfo.avatar || '/static/icons/user_icon.png'
+	userData.userId = userInfo.userid
+	
+	if (storage.getHasLogin()) {
+		isLogin.value = true
+		// 加载当前登录用户所属所有地块信�?
+		const cached = storage.getCurrentUserPlotsList()
+		if (Array.isArray(cached) && cached.length > 0) {
+			// 如果有缓存,直接用缓存构�?columns
+			columns.value = [cached]
+			console.log("从缓存加载字段:", columns.value)
+			// 有缓存时,在columns设置后立即设置defaultIndex
+			setDefaultSelectedPlot()
+		} else {
+			// 无缓存时请求接口,在接口完成后会调用setDefaultSelectedPlot
+			loadPhots(userData.userId)
 		}
-	};
+		console.log("columns", columns.value)
+	}
+})
+
+// onMounted 也保留,用于首次加载
+onMounted(() => {
+	const userInfo = storage.getUserInfo()
+	console.log("userInfo", userInfo)
+	userData.nickname = userInfo.username || '未登录'
+	userData.avatar = userInfo.avatar || '/static/icons/user_icon.png'
+	userData.userId = userInfo.userid
+	
+	if (storage.getHasLogin()) {
+		isLogin.value = true
+		// 加载当前登录用户所属所有地块信�?
+		const cached = storage.getCurrentUserPlotsList()
+		if (Array.isArray(cached) && cached.length > 0) {
+			// 如果有缓存,直接用缓存构�?columns
+			columns.value = [cached]
+			console.log("从缓存加载字�?", columns.value)
+			// 有缓存时,在columns设置后立即设置defaultIndex
+			setDefaultSelectedPlot()
+		} else {
+			// 无缓存时请求接口,在接口完成后会调用setDefaultSelectedPlot
+			loadPhots(userData.userId)
+		}
+		console.log("columns", columns.value)
+	}
+})
 </script>
 
 <style lang="scss" scoped>

+ 1596 - 0
pages/dashboard/index_back.vue

@@ -0,0 +1,1596 @@
+<template>
+	<view class="dashboard-container">
+		<!-- 顶部用户信息卡片 -->
+		<view class="user-info-card">
+			<view class="user-info">
+				<text class="greeting">您好,{{ userData.nickname }}</text>
+				<view class="plot-info">
+					<text class="plot-label">当前地块:</text>
+					<text class="plot-name">{{ userData.selectedPlot }}</text>
+					<view class="switch-plot-btn" v-if="isLogin">
+						<up-picker :show="show" :columns="columns" :defaultIndex="defaultIndex" @cancel="show = false"
+							@confirm="onConfirm"></up-picker>
+						<text @click="handleSwitchPlot">切换</text>
+					</view>
+
+				</view>
+			</view>
+			<view class="avatar-container" @click="navigateToProfile">
+				<view class="avatar">
+					<image :src="userData.avatar" mode="aspectFill"></image>
+				</view>
+			</view>
+		</view>
+
+		<!-- 顶部统计概览 -->
+		<view class="stats-overview">
+			<view class="alert-card" v-for="(alert, index) in alertSummaries" :key="index"
+				@click="navigateToAlertDetail(alert.type)">
+				<view class="alert-header">
+					<view class="alert-icon-container">
+						<image class="custom-icon" :src="alert.iconSrc"></image>
+					</view>
+					<text class="alert-title">{{ alert.title }}</text>
+				</view>
+				<text class="alert-value">{{ alert.value }}</text>
+				<text class="alert-description">{{ alert.description }}</text>
+			</view>
+		</view>
+
+		<!-- 天气卡片 -->
+		<view class="card weather-card">
+			<view class="card-header">
+				<view class="title-section">
+					<view class="title-line"></view>
+					<text class="card-title">天气与预报</text>
+				</view>
+			</view>
+			<view class="weather-content">
+				<view class="current-weather">
+					<view class="weather-icon-container">
+						<view class="weather-icon">
+							<text v-if="weatherData.description === '晴朗'">☀️</text>
+							<text v-else-if="weatherData.description.includes('雨')">🌧️</text>
+							<text v-else-if="weatherData.description.includes('云')">⛅</text>
+							<text v-else>🌤️</text>
+						</view>
+					</view>
+					<view class="weather-details">
+						<text class="weather-temp">{{ weatherData.temperature }}°C</text>
+						<text class="weather-desc">{{ weatherData.description }}</text>
+					</view>
+				</view>
+				<view class="weather-metrics">
+					<view class="weather-metric">
+						<text class="metric-label">湿度</text>
+						<text class="metric-value">{{ weatherData.humidity }}%</text>
+					</view>
+					<view class="vertical-divider"></view>
+					<view class="weather-metric">
+						<text class="metric-label">风力</text>
+						<text class="metric-value">{{ weatherData.windLevel }} 级</text>
+					</view>
+					<view class="vertical-divider"></view>
+					<view class="weather-metric">
+						<text class="metric-label">降水量</text>
+						<text class="metric-value">{{ weatherData.rainfall }} mm</text>
+					</view>
+				</view>
+				<view class="weather-advice">
+					<view class="advice-header">
+						<view class="icon-tile small">
+							<up-icon name="info-circle" color="#ffffff" size="14"></up-icon>
+						</view>
+						<text class="advice-title">今日建议:</text>
+					</view>
+					<text class="advice-content">{{ weatherData.advice }}</text>
+				</view>
+			</view>
+		</view>
+
+		<!-- 农场绩效卡片 -->
+		<view class="card farm-performance">
+			<view class="card-header">
+				<view class="title-section">
+					<view class="title-line"></view>
+					<text class="card-title">产值分析</text>
+				</view>
+				<view class="period-selector">
+					<text class="period" :class="{ active: currentPeriod === 'month' }"
+						@click="changePeriod('month')">月</text>
+					<text class="period" :class="{ active: currentPeriod === 'quarter' }"
+						@click="changePeriod('quarter')">季</text>
+					<text class="period" :class="{ active: currentPeriod === 'year' }"
+						@click="changePeriod('year')">年</text>
+				</view>
+			</view>
+			<view class="performance-stats">
+				<view class="metric-column">
+					<text class="metric-value">128<text class="metric-unit">亩</text></text>
+					<text class="metric-label">管理面积总计</text>
+					<view class="growth positive">
+						<up-icon name="arrow-up" color="#3BB44A" size="12"></up-icon>
+						<text>比上月增长12%</text>
+					</view>
+				</view>
+				<view class="metric-divider"></view>
+				<view class="metric-column">
+					<text class="metric-value">¥36,480</text>
+					<text class="metric-label">预计产值</text>
+					<view class="growth positive">
+						<up-icon name="arrow-up" color="#3BB44A" size="12"></up-icon>
+						<text>比上月增长8.2%</text>
+					</view>
+				</view>
+			</view>
+			<view class="chart-container">
+				<view class="chart-header">
+					<text class="chart-title">产值趋势 (万元)</text>
+					<view class="chart-legend">
+						<view class="legend-item">
+							<view class="legend-color" style="background: #3BB44A;"></view>
+							<text>今年</text>
+						</view>
+						<view class="legend-item">
+							<view class="legend-color" style="background: #E0E0E0;"></view>
+							<text>去年</text>
+						</view>
+					</view>
+				</view>
+				<view class="chart-body">
+					<view class="y-axis">
+						<text v-for="(value, index) in [4, 3, 2, 1, 0]" :key="index">{{ value }}</text>
+					</view>
+					<view class="bars-container">
+						<view class="month-group" v-for="(month, index) in farmPerformanceData.months" :key="index">
+							<view class="bar-wrapper">
+								<view class="bar last-year"
+									:style="{ height: (farmPerformanceData.lastYearValues[index] / 4) * 100 + '%' }">
+								</view>
+								<view class="bar this-year"
+									:style="{ height: (farmPerformanceData.thisYearValues[index] / 4) * 100 + '%' }">
+								</view>
+							</view>
+							<text class="month-label">{{ month }}</text>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 监控设备概览 -->
+		<view class="card device-overview">
+			<view class="card-header">
+				<view class="title-section">
+					<view class="title-line"></view>
+					<text class="card-title">监控设备概览</text>
+				</view>
+			</view>
+			<view class="device-metrics-grid">
+				<view class="device-metric-card" v-for="(metric, index) in deviceMetrics" :key="index">
+					<view class="device-metric-header">
+						<view class="icon-tile" :style="{ background: metric.gradient }">
+							<up-icon :name="metric.icon" color="#ffffff" size="18"></up-icon>
+						</view>
+						<text class="metric-name">{{ metric.name }}</text>
+					</view>
+					<view class="device-metric-value">{{ metric.value }}</view>
+					<view class="device-metric-trend" :class="metric.trend.type">
+						<up-icon :name="metric.trend.type === 'up' ? 'arrow-up' : 'arrow-down'"
+							:color="metric.trend.type === 'up' ? '#3BB44A' : '#FF5252'" size="14"></up-icon>
+						<text>{{ metric.trend.value }}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 农业机械活动 -->
+		<view class="card machinery-activity">
+			<view class="card-header">
+				<view class="title-section">
+					<view class="title-line"></view>
+					<text class="card-title">农机作业概览</text>
+				</view>
+			</view>
+			<view class="machinery-metrics-grid">
+				<view class="machinery-metric-card" v-for="(metric, index) in machineryMetrics" :key="index">
+					<view class="machinery-metric-header">
+						<view class="icon-tile" :style="{ background: metric.gradient }">
+							<up-icon :name="metric.icon" color="#ffffff" size="18"></up-icon>
+						</view>
+						<text class="metric-name">{{ metric.name }}</text>
+					</view>
+					<view class="machinery-metric-value">{{ metric.value }}</view>
+					<view v-if="metric.trend" class="machinery-metric-trend" :class="metric.trend.type">
+						<up-icon :name="metric.trend.type === 'up' ? 'arrow-up' : 'arrow-down'"
+							:color="metric.trend.type === 'up' ? '#3BB44A' : '#FF5252'" size="14"></up-icon>
+						<text>{{ metric.trend.value }}</text>
+					</view>
+					<view v-else class="machinery-metric-unit">
+						<text>{{ metric.unit }}</text>
+					</view>
+				</view>
+			</view>
+		</view>
+
+		<!-- 农场活动卡片 -->
+		<view class="card farm-activities">
+			<view class="card-header">
+				<view class="title-section">
+					<view class="title-line"></view>
+					<text class="card-title">农事活动</text>
+				</view>
+				<view class="action-button">
+					<text>查看全部</text>
+					<up-icon name="arrow-right" color="#ffffff" size="14"></up-icon>
+				</view>
+			</view>
+			<view class="activities-list">
+				<view class="activity-item" v-for="(activity, index) in farmData.recentActivities" :key="index">
+					<view class="activity-dot" :class="activity.status"></view>
+					<view class="activity-details">
+						<view class="activity-title-row">
+							<text class="activity-title">{{ activity.title }}</text>
+							<text class="activity-date">{{ activity.date }}</text>
+						</view>
+						<view class="activity-meta-row">
+							<text class="activity-executor">{{ activity.executor }}</text>
+							<view class="activity-action" @click.stop="navigateToActivity(activity)">
+								<text class="action-text">查看</text>
+								<up-icon name="arrow-right" color="#3BB44A" size="14"></up-icon>
+							</view>
+						</view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	import storage from "@/utils/storage.js";
+	import {
+		listFieldName
+	} from "@/api/services/field.js"
+	export default {
+		data() {
+			return {
+				isLogin:false,
+				show: false,
+				columns: [
+					[]
+				],
+				defaultIndex: [0],
+				// 用户数据
+				userData: {
+					nickname: '游客',
+					selectedPlot: '未选择地块',
+					avatar: '/static/images/user-avatar.png',
+				},
+
+				// 农场数据
+				farmData: {
+					plotCount: 5,
+					deviceCount: 12,
+					deviceOnlineRate: 85,
+					taskCompletionRate: 76,
+					recentActivities: [{
+							title: '稻田水稻施肥',
+							date: '2023-05-15',
+							status: 'pending',
+							id: 1,
+							executor: '李四'
+						},
+						{
+							title: '南地块除草',
+							date: '2023-05-12',
+							status: 'completed',
+							id: 2,
+							executor: '王五'
+						},
+						{
+							title: '西北区域杀虫',
+							date: '2023-05-08',
+							status: 'completed',
+							id: 3,
+							executor: '张三'
+						}
+					]
+				},
+
+				// 天气数据
+				weatherData: {
+					temperature: 28,
+					description: '晴朗',
+					humidity: 65,
+					windLevel: 3,
+					rainfall: 0,
+					advice: '今日适宜进行春玉米防虫作业,注意水分管理。'
+				},
+
+				// 作物数据
+				crops: [{
+						name: '水稻',
+						area: 48,
+						progress: 75,
+						icon: '🌾',
+						bgColor: '#4CAF50'
+					},
+					{
+						name: '小麦',
+						area: 36,
+						progress: 60,
+						icon: '🌿',
+						bgColor: '#66BB6A'
+					},
+					{
+						name: '玉米',
+						area: 32,
+						progress: 85,
+						icon: '🌽',
+						bgColor: '#43A047'
+					},
+					{
+						name: '大豆',
+						area: 12,
+						progress: 40,
+						icon: '🫘',
+						bgColor: '#388E3C'
+					}
+				],
+
+				// 监控设备指标
+				deviceMetrics: [{
+						name: '在线设备',
+						value: '28',
+						icon: 'wifi',
+						gradient: 'linear-gradient(135deg, #26A69A, #00796B)',
+						trend: {
+							type: 'up',
+							value: '5.2%'
+						}
+					},
+					{
+						name: '告警设备',
+						value: '3',
+						icon: 'error-circle',
+						gradient: 'linear-gradient(135deg, #FF7043, #E64A19)',
+						trend: {
+							type: 'down',
+							value: '2.1%'
+						}
+					},
+					{
+						name: '离线设备',
+						value: '5',
+						icon: 'close-circle',
+						gradient: 'linear-gradient(135deg, #78909C, #455A64)',
+						trend: {
+							type: 'down',
+							value: '1.8%'
+						}
+					},
+					{
+						name: '数据稳定性',
+						value: '96.3%',
+						icon: 'checkmark-circle',
+						gradient: 'linear-gradient(135deg, #66BB6A, #388E3C)',
+						trend: {
+							type: 'up',
+							value: '0.5%'
+						}
+					}
+				],
+
+				// 农业机械指标
+				machineryMetrics: [{
+						name: '今日运行时长',
+						value: '36.5',
+						unit: '小时',
+						icon: 'clock',
+						gradient: 'linear-gradient(135deg, #42A5F5, #1976D2)'
+					},
+					{
+						name: '今日作业地块',
+						value: '8',
+						unit: '块',
+						icon: 'map',
+						gradient: 'linear-gradient(135deg, #66BB6A, #388E3C)'
+					},
+					{
+						name: '今日执行任务',
+						value: '12',
+						unit: '个',
+						icon: 'calendar',
+						gradient: 'linear-gradient(135deg, #FFA726, #F57C00)'
+					},
+					{
+						name: '使用率',
+						value: '78.2%',
+						icon: 'star',
+						gradient: 'linear-gradient(135deg, #5C6BC0, #3949AB)',
+						trend: {
+							type: 'up',
+							value: '3.7%'
+						}
+					}
+				],
+
+				// 当前选择的周期
+				currentPeriod: 'month',
+
+				// 警报摘要数据
+				alertSummaries: [{
+						title: '设备离线',
+						value: '3 台',
+						iconSrc: '/static/icons/offline.png',
+						description: '最长离线时长:26 小时',
+						type: 'device-offline'
+					},
+					{
+						title: '虫害预警',
+						value: '稻飞虱|小地老虎',
+						iconSrc: '/static/icons/Pest_Alert.png',
+						description: '预警等级:中',
+						type: 'pest-warning'
+					},
+					{
+						title: '气象预警',
+						value: '强风(8级)',
+						iconSrc: '/static/icons/weather_risk.png',
+						description: '预计:未来 12 小时内',
+						type: 'weather-risk'
+					},
+					{
+						title: '作业延迟',
+						value: '5 项',
+						iconSrc: '/static/icons/task_delay.png',
+						description: '最长延迟:3 天',
+						type: 'task-delay'
+					}
+				],
+
+				// 农场绩效图表数据 - 月度数据
+				monthlyData: {
+					months: ['1月', '2月', '3月', '4月', '5月', '6月'],
+					thisYearValues: [2.8, 3.4, 2.9, 3.6, 3.8, 3.2],
+					lastYearValues: [2.5, 2.8, 2.4, 3.0, 3.2, 2.7]
+				},
+
+				// 季度数据
+				quarterlyData: {
+					months: ['Q1', 'Q2', 'Q3', 'Q4'],
+					thisYearValues: [3.2, 3.7, 4.0, 3.5],
+					lastYearValues: [2.7, 3.3, 3.6, 3.0]
+				},
+
+				// 年度数据
+				yearlyData: {
+					months: ['2019', '2020', '2021', '2022', '2023'],
+					thisYearValues: [2.2, 2.5, 3.0, 3.5, 3.8],
+					lastYearValues: [2.0, 2.3, 2.7, 3.2, 3.4]
+				}
+			};
+		},
+
+		computed: {
+			// 核心统计数据
+			coreStats() {
+				return [{
+						label: '地块',
+						value: this.farmData.plotCount,
+						icon: 'map',
+					},
+					{
+						label: '设备',
+						value: this.farmData.deviceCount,
+						icon: 'setting',
+					},
+					{
+						label: '设备在线',
+						value: this.farmData.deviceOnlineRate + '%',
+						icon: 'wifi',
+					},
+					{
+						label: '任务完成',
+						value: this.farmData.taskCompletionRate + '%',
+						icon: 'checkmark-circle',
+					}
+				];
+			},
+
+			// 根据当前周期计算要显示的数据
+			farmPerformanceData() {
+				if (this.currentPeriod === 'month') {
+					return this.monthlyData;
+				} else if (this.currentPeriod === 'quarter') {
+					return this.quarterlyData;
+				} else {
+					return this.yearlyData;
+				}
+			}
+		},
+
+		methods: {
+			loadPhots(userId) {
+				console.log("加载用户字段", userId);
+
+				listFieldName(userId)
+					.then(res => {
+						const {
+							data
+						} = res || {};
+
+						if (data?.code !== 200 || !Array.isArray(data.data)) {
+							console.warn("接口返回异常或数据格式不正确", res);
+							return;
+						}
+
+						const fieldList = data.data;
+						console.log("fieldList:",fieldList);
+						const fields = [];
+						fieldList.forEach(item => {
+							const fieldName = item?.fieldName;
+							if (fieldName && !fields.includes(fieldName)) {
+								fields.push({
+									text: fieldName,
+									value: item.id,
+									growCrops: item.growCrops,
+									managerName: item.managerName,
+									size: item.size,
+									farmId: item.farmId
+								});
+							}
+						});
+
+						this.columns = [fields]; // 符合 u-picker 的二维数组格式
+						storage.setCurrentUserPlotsList(fields)
+
+						console.log("字段加载完成:", this.columns);
+						
+						// 字段加载完成后立即设置默认索引
+						this.setDefaultSelectedPlot();
+					})
+					.catch(err => {
+						console.error("加载字段失败", err);
+					});
+			},
+
+			// 初始化地块列表
+			initPlotColumns() {
+				const cached = storage.getCurrentUserPlotsList();
+				if (Array.isArray(cached) && cached.length > 0) {
+					this.columns = [cached]; // 结构上是二维数组
+					console.log("从缓存加载字段:", this.columns);
+				} else {
+					this.loadPhots(this.userData.userid); // 异步方法内部要处理赋值 columns
+				}
+			},
+
+			// 设置默认选中项
+			setDefaultSelectedPlot() {
+				try {
+					const currentPlots = JSON.parse(storage.getPlots() || '{}');
+					console.log("设置默认地块", currentPlots);
+					
+					if (currentPlots?.name) {
+						this.userData.selectedPlot = currentPlots.name;
+
+						// 防止 this.columns 未定义或格式不正确
+						if (Array.isArray(this.columns) && this.columns.length > 0 && Array.isArray(this.columns[0])) {
+							const plots = this.columns[0];
+							const index = plots.findIndex(item => item.text === currentPlots.name);
+
+							if (index !== -1) {
+								this.defaultIndex = [index];
+								console.log("设置默认选中地块索引:", index, "defaultIndex:", this.defaultIndex);
+							} else {
+								console.warn("未找到匹配的地块:", currentPlots.name);
+								this.defaultIndex = [0]; // 默认选中第一项
+							}
+						} else {
+							console.warn("columns数据格式不正确:", this.columns);
+							this.defaultIndex = [0]; // 默认选中第一项
+						}
+					} else {
+						console.warn("未设置默认地块或地块名为空");
+						this.defaultIndex = [0]; // 默认选中第一项
+					}
+				} catch (error) {
+					console.error("设置默认地块时出错:", error);
+					this.defaultIndex = [0]; // 出错时默认选中第一项
+				}
+			},
+			onConfirm(e) {
+				console.log('选择了:', e);
+				this.userData.selectedPlot = e.value[0].text
+				let obj = {id:e.value[0].value,name:e.value[0].text,growCrops:e.value[0].growCrops,managerName:e.value[0].managerName,size:e.value[0].size,farmId: e.value[0].farmId}
+				storage.setPlots(JSON.stringify(obj))
+				this.show = false;
+			},
+			// 处理切换地块
+			handleSwitchPlot() {
+				// 在显示picker前重新设置defaultIndex
+				this.setDefaultSelectedPlot();
+				this.show = true;
+			},
+
+			// 导航到个人资料
+			navigateToProfile() {
+				uni.navigateTo({
+					url: '/pages/user/index'
+				});
+			},
+
+			// 导航到活动详情
+			navigateToActivity(activity) {
+				uni.navigateTo({
+					url: `/pages/activity/detail?id=${activity.id}`
+				});
+			},
+
+			// 导航到警报详情
+			navigateToAlertDetail(type) {
+				// 实现导航到警报详情页的逻辑
+				console.log(`Navigating to alert detail for type: ${type}`);
+			},
+
+			// 切换周期
+			changePeriod(period) {
+				this.currentPeriod = period;
+			}
+		},
+		onShow() {
+			const userInfo = storage.getUserInfo()
+			console.log("userInfo", userInfo);
+			this.userData.nickname = userInfo.username || '未登录'
+			this.userData.avatar = userInfo.avatar || '/static/icons/user_icon.png'
+			this.userData.userId = userInfo.userid
+			
+			if(storage.getHasLogin()){
+				this.isLogin = true;
+				// 加载当前登录用户所属所有地块信息
+				const cached = storage.getCurrentUserPlotsList();
+				if (Array.isArray(cached) && cached.length > 0) {
+					// 如果有缓存,直接用缓存构造 columns
+					this.columns = [cached];
+					console.log("从缓存加载字段:", this.columns);
+					// 有缓存时,在columns设置后立即设置defaultIndex
+					this.setDefaultSelectedPlot();
+				} else {
+					// 无缓存时请求接口,在接口完成后会调用setDefaultSelectedPlot
+					this.loadPhots(this.userData.userId);
+				}
+				console.log("columns", this.columns);
+			}
+
+		},
+
+		mounted() {
+			
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	.dashboard-container {
+		padding: 24rpx;
+		background-color: #F6FDF9;
+		min-height: 100vh;
+	}
+
+	// 顶部用户信息卡片
+	.user-info-card {
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
+		background: white;
+		border-radius: 20rpx;
+		padding: 24rpx 28rpx;
+		margin-bottom: 24rpx;
+		box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
+
+		.user-info {
+			flex: 1;
+
+			.greeting {
+				font-size: 32rpx;
+				font-weight: 600;
+				color: #2C3E50;
+				margin-bottom: 8rpx;
+			}
+
+			.plot-info {
+				display: flex;
+				align-items: center;
+
+				.plot-label {
+					font-size: 24rpx;
+					color: #8C9396;
+				}
+
+				.plot-name {
+					font-size: 24rpx;
+					color: #2C3E50;
+					margin: 0 8rpx;
+				}
+
+				.switch-plot-btn {
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					background: linear-gradient(135deg, #3BB44A, #66CC6A);
+					border-radius: 16rpx;
+					padding: 4rpx 16rpx;
+					box-shadow: 0 2rpx 8rpx rgba(59, 180, 74, 0.25);
+
+					text {
+						font-size: 22rpx;
+						color: white;
+					}
+				}
+			}
+		}
+
+		.avatar-container {
+			.avatar {
+				width: 80rpx;
+				height: 80rpx;
+				border-radius: 50%;
+				background-color: rgba(59, 180, 74, 0.1);
+				border: 2px solid rgba(255, 255, 255, 0.8);
+				box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+				overflow: hidden;
+
+				image {
+					width: 100%;
+					height: 100%;
+					object-fit: cover;
+				}
+			}
+		}
+	}
+
+	// Top Stats Overview
+	.stats-overview {
+		display: grid;
+		grid-template-columns: repeat(2, 1fr);
+		gap: 16rpx;
+		margin-bottom: 24rpx;
+
+		.alert-card {
+			background: white;
+			border-radius: 16rpx;
+			padding: 20rpx;
+			display: flex;
+			flex-direction: column;
+			box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
+			position: relative;
+			overflow: hidden;
+			transition: transform 0.2s, box-shadow 0.2s;
+
+			&:active {
+				transform: translateY(2rpx);
+				box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
+			}
+
+			&::after {
+				content: "";
+				position: absolute;
+				right: 12rpx;
+				bottom: 12rpx;
+				width: 16rpx;
+				height: 16rpx;
+				border-top: 2rpx solid #E0E0E0;
+				border-right: 2rpx solid #E0E0E0;
+				transform: rotate(45deg);
+				opacity: 0.5;
+			}
+
+			.alert-header {
+				display: flex;
+				align-items: center;
+				margin-bottom: 12rpx;
+
+				.alert-icon-container {
+					margin-right: 12rpx;
+					width: 40rpx;
+					height: 40rpx;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+
+					.custom-icon {
+						width: 40rpx;
+						height: 40rpx;
+						object-fit: contain;
+					}
+				}
+
+				.alert-title {
+					font-size: 26rpx;
+					color: #333333;
+					font-weight: 600;
+				}
+			}
+
+			.alert-value {
+				font-size: 30rpx;
+				font-weight: 700;
+				color: #3BB44A;
+				margin-bottom: 6rpx;
+				line-height: 1.2;
+				padding-left: 52rpx;
+			}
+
+			.alert-description {
+				font-size: 22rpx;
+				color: #757575;
+				padding-left: 52rpx;
+			}
+		}
+	}
+
+	// Card Base Styles
+	.card {
+		background: white;
+		border-radius: 20rpx;
+		box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04), 0 1rpx 4rpx rgba(0, 0, 0, 0.02);
+		margin-bottom: 24rpx;
+		overflow: hidden;
+
+		.card-header {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			padding: 24rpx 28rpx;
+			border-bottom: 1px solid rgba(0, 0, 0, 0.03);
+
+			.title-section {
+				display: flex;
+				align-items: center;
+
+				.title-line {
+					width: 4rpx;
+					height: 28rpx;
+					border-radius: 0;
+					background: linear-gradient(180deg, #3BB44A, #66CC6A);
+					margin-right: 16rpx;
+				}
+
+				.card-title {
+					font-size: 28rpx;
+					font-weight: 600;
+					color: #2C3E50;
+				}
+			}
+
+			.period-selector {
+				display: flex;
+				background: rgba(0, 0, 0, 0.03);
+				border-radius: 20rpx;
+				padding: 2rpx;
+
+				.period {
+					font-size: 22rpx;
+					color: #8C9396;
+					padding: 8rpx 16rpx;
+					border-radius: 16rpx;
+
+					&.active {
+						background: white;
+						color: #3BB44A;
+						font-weight: 500;
+						box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
+					}
+				}
+			}
+
+			.action-button {
+				display: flex;
+				align-items: center;
+				background: linear-gradient(135deg, #3BB44A, #66CC6A);
+				border-radius: 24rpx;
+				padding: 8rpx 16rpx;
+				box-shadow: 0 2rpx 8rpx rgba(59, 180, 74, 0.25);
+
+				text {
+					font-size: 22rpx;
+					color: white;
+					margin-right: 6rpx;
+				}
+			}
+		}
+	}
+
+	// Farm Performance Card
+	.farm-performance {
+		.performance-stats {
+			display: flex;
+			padding: 24rpx 28rpx;
+
+			.metric-column {
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+
+				.metric-value {
+					font-size: 48rpx;
+					font-weight: 700;
+					color: #2C3E50;
+					margin-bottom: 6rpx;
+					line-height: 1.1;
+
+					.metric-unit {
+						font-size: 24rpx;
+						font-weight: 500;
+						color: #8C9396;
+						margin-left: 4rpx;
+					}
+				}
+
+				.metric-label {
+					font-size: 24rpx;
+					color: #8C9396;
+					margin-bottom: 12rpx;
+				}
+
+				.growth {
+					display: flex;
+					align-items: center;
+					font-size: 22rpx;
+
+					&.positive {
+						color: #3BB44A;
+					}
+
+					&.negative {
+						color: #FF5252;
+					}
+				}
+			}
+
+			.metric-divider {
+				width: 1px;
+				background: rgba(0, 0, 0, 0.04);
+				margin: 0 28rpx;
+			}
+		}
+
+		.chart-container {
+			padding: 24rpx 28rpx;
+
+			.chart-header {
+				display: flex;
+				justify-content: space-between;
+				align-items: center;
+				margin-bottom: 16rpx;
+
+				.chart-title {
+					font-size: 28rpx;
+					font-weight: 600;
+					color: #2C3E50;
+				}
+
+				.chart-legend {
+					display: flex;
+					align-items: center;
+
+					.legend-item {
+						display: flex;
+						align-items: center;
+						margin-left: 16rpx;
+
+						.legend-color {
+							width: 16rpx;
+							height: 16rpx;
+							border-radius: 4rpx;
+							margin-right: 8rpx;
+						}
+
+						text {
+							font-size: 22rpx;
+							color: #8C9396;
+						}
+					}
+				}
+			}
+
+			.chart-body {
+				display: flex;
+				height: 240rpx;
+				position: relative;
+				margin-top: 12rpx;
+
+				.y-axis {
+					width: 40rpx;
+					height: 200rpx;
+					display: flex;
+					flex-direction: column;
+					justify-content: space-between;
+					text-align: right;
+					padding-right: 16rpx;
+					margin-bottom: 30rpx;
+
+					text {
+						font-size: 22rpx;
+						color: #8C9396;
+					}
+				}
+
+				.bars-container {
+					flex: 1;
+					display: flex;
+					justify-content: space-around;
+					height: 200rpx;
+					position: relative;
+
+					&::before {
+						content: "";
+						position: absolute;
+						left: 0;
+						bottom: 0;
+						width: 100%;
+						height: 1px;
+						background-color: #E0E0E0;
+					}
+
+					.month-group {
+						display: flex;
+						flex-direction: column;
+						align-items: center;
+						width: 14%;
+
+						.bar-wrapper {
+							height: 200rpx;
+							width: 70%;
+							display: flex;
+							justify-content: center;
+							position: relative;
+
+							.bar {
+								position: absolute;
+								bottom: 0;
+								width: 45%;
+								border-radius: 4rpx 4rpx 0 0;
+								transition: height 0.3s ease;
+							}
+
+							.last-year {
+								left: 0;
+								background: #E0E0E0;
+							}
+
+							.this-year {
+								right: 0;
+								background: #3BB44A;
+							}
+						}
+
+						.month-label {
+							font-size: 22rpx;
+							color: #8C9396;
+							margin-top: 8rpx;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	// Crop Portfolio
+	.crop-portfolio {
+		.crop-grid {
+			padding: 16rpx;
+			display: grid;
+			grid-template-columns: repeat(2, 1fr);
+			gap: 16rpx;
+		}
+
+		.crop-item {
+			padding: 16rpx;
+			background: rgba(0, 0, 0, 0.01);
+			border-radius: 16rpx;
+			display: flex;
+			align-items: center;
+
+			.crop-icon {
+				width: 48rpx;
+				height: 48rpx;
+				border-radius: 12rpx;
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				margin-right: 16rpx;
+
+				.crop-icon-text {
+					font-size: 24rpx;
+				}
+			}
+
+			.crop-details {
+				flex: 1;
+
+				.crop-info {
+					display: flex;
+					justify-content: space-between;
+					align-items: center;
+					margin-bottom: 10rpx;
+
+					.crop-name {
+						font-size: 26rpx;
+						font-weight: 600;
+						color: #2C3E50;
+					}
+
+					.crop-area {
+						font-size: 22rpx;
+						color: #8C9396;
+					}
+				}
+
+				.crop-stats {
+					display: flex;
+					align-items: center;
+
+					.crop-progress-container {
+						flex: 1;
+						height: 8rpx;
+						background: rgba(0, 0, 0, 0.03);
+						border-radius: 4rpx;
+						margin-right: 12rpx;
+						overflow: hidden;
+
+						.crop-progress {
+							height: 100%;
+							border-radius: 4rpx;
+						}
+					}
+
+					.crop-progress-text {
+						font-size: 22rpx;
+						color: #8C9396;
+						min-width: 36rpx;
+						text-align: right;
+					}
+				}
+			}
+		}
+	}
+
+	// Resource Efficiency
+	.resource-efficiency {
+		.resource-grid {
+			padding: 16rpx;
+			display: grid;
+			grid-template-columns: repeat(2, 1fr);
+			gap: 16rpx;
+		}
+
+		.resource-item {
+			padding: 16rpx;
+			background: rgba(0, 0, 0, 0.01);
+			border-radius: 16rpx;
+
+			.resource-header {
+				display: flex;
+				align-items: center;
+				margin-bottom: 12rpx;
+
+				.icon-tile {
+					width: 28rpx;
+					height: 28rpx;
+					border-radius: 6rpx;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					margin-right: 10rpx;
+
+					&.small {
+						width: 28rpx;
+						height: 28rpx;
+					}
+				}
+
+				.resource-name {
+					font-size: 24rpx;
+					color: #8C9396;
+				}
+			}
+
+			.resource-value {
+				margin-bottom: 12rpx;
+
+				.value {
+					font-size: 36rpx;
+					font-weight: 700;
+					color: #2C3E50;
+				}
+
+				.unit {
+					font-size: 22rpx;
+					color: #8C9396;
+					margin-left: 4rpx;
+				}
+			}
+
+			.resource-progress-container {
+				.resource-progress-bg {
+					height: 8rpx;
+					background: rgba(0, 0, 0, 0.03);
+					border-radius: 4rpx;
+					margin-bottom: 8rpx;
+					overflow: hidden;
+
+					.resource-progress {
+						height: 100%;
+						border-radius: 4rpx;
+					}
+				}
+
+				.resource-efficiency {
+					font-size: 22rpx;
+					color: #8C9396;
+				}
+			}
+		}
+	}
+
+	// Farm Activities
+	.farm-activities {
+		.activities-list {
+			padding: 8rpx 0;
+		}
+
+		.activity-item {
+			display: flex;
+			align-items: center;
+			padding: 20rpx 28rpx;
+			border-bottom: 1px solid rgba(0, 0, 0, 0.02);
+
+			&:last-child {
+				border-bottom: none;
+			}
+
+			.activity-dot {
+				width: 10rpx;
+				height: 10rpx;
+				border-radius: 50%;
+				margin-right: 16rpx;
+				flex-shrink: 0;
+
+				&.completed {
+					background: #4CAF50;
+				}
+
+				&.pending {
+					background: #FFC107;
+				}
+
+				&.failed {
+					background: #FF5252;
+				}
+			}
+
+			.activity-details {
+				flex: 1;
+
+				.activity-title-row {
+					display: flex;
+					justify-content: space-between;
+					align-items: center;
+					margin-bottom: 6rpx;
+
+					.activity-title {
+						font-size: 26rpx;
+						font-weight: 500;
+						color: #2C3E50;
+					}
+
+					.activity-date {
+						font-size: 22rpx;
+						color: #8C9396;
+					}
+				}
+
+				.activity-meta-row {
+					display: flex;
+					justify-content: space-between;
+					align-items: center;
+
+					.activity-executor {
+						font-size: 22rpx;
+						color: #8C9396;
+					}
+
+					.activity-action {
+						display: flex;
+						align-items: center;
+
+						.action-text {
+							font-size: 22rpx;
+							color: #3BB44A;
+							margin-right: 6rpx;
+						}
+					}
+				}
+			}
+		}
+	}
+
+	// Weather Card
+	.weather-card {
+		.weather-content {
+			padding: 20rpx 28rpx;
+		}
+
+		.current-weather {
+			display: flex;
+			align-items: center;
+			margin-bottom: 24rpx;
+
+			.weather-icon-container {
+				margin-right: 20rpx;
+
+				.weather-icon {
+					font-size: 60rpx;
+					line-height: 1;
+				}
+			}
+
+			.weather-details {
+				.weather-temp {
+					font-size: 48rpx;
+					font-weight: 700;
+					color: #2C3E50;
+					line-height: 1.1;
+				}
+
+				.weather-desc {
+					font-size: 26rpx;
+					color: #8C9396;
+				}
+			}
+		}
+
+		.weather-metrics {
+			display: flex;
+			justify-content: space-between;
+			align-items: center;
+			padding: 16rpx 24rpx;
+			margin-bottom: 24rpx;
+			border-radius: 12rpx;
+			background-color: rgba(247, 247, 247, 0.5);
+			border: 1px solid rgba(0, 0, 0, 0.03);
+
+			.weather-metric {
+				flex: 1;
+				display: flex;
+				flex-direction: column;
+				align-items: center;
+
+				.metric-label {
+					font-size: 22rpx;
+					color: #8C9396;
+					margin-bottom: 6rpx;
+				}
+
+				.metric-value {
+					font-size: 28rpx;
+					font-weight: 600;
+					color: #2C3E50;
+				}
+			}
+
+			.vertical-divider {
+				width: 1px;
+				height: 36rpx;
+				background-color: rgba(0, 0, 0, 0.05);
+			}
+		}
+
+		.weather-advice {
+			background: rgba(59, 180, 74, 0.05);
+			border-radius: 16rpx;
+			padding: 16rpx;
+
+			.advice-header {
+				display: flex;
+				align-items: center;
+				margin-bottom: 10rpx;
+
+				.icon-tile {
+					width: 24rpx;
+					height: 24rpx;
+					border-radius: 50%;
+					background: linear-gradient(135deg, #3BB44A, #66CC6A);
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					margin-right: 10rpx;
+					box-shadow: 0 2rpx 8rpx rgba(59, 180, 74, 0.25);
+
+					&.small {
+						width: 24rpx;
+						height: 24rpx;
+					}
+				}
+
+				.advice-title {
+					font-size: 24rpx;
+					font-weight: 600;
+					color: #2C3E50;
+				}
+			}
+
+			.advice-content {
+				font-size: 24rpx;
+				color: #2C3E50;
+				line-height: 1.4;
+			}
+		}
+	}
+
+	// For small screens - mobile responsiveness
+	@media screen and (max-width: 768px) {
+		.stats-overview {
+			grid-template-columns: repeat(2, 1fr);
+		}
+
+		.crop-grid {
+			grid-template-columns: 1fr;
+		}
+	}
+
+	// 监控设备概览
+	.device-overview {
+		.device-metrics-grid {
+			display: grid;
+			grid-template-columns: repeat(2, 1fr);
+			gap: 16rpx;
+			padding: 16rpx;
+		}
+
+		.device-metric-card {
+			background: white;
+			border-radius: 16rpx;
+			padding: 20rpx;
+			box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
+			border: 1px solid rgba(0, 0, 0, 0.02);
+
+			.device-metric-header {
+				display: flex;
+				align-items: center;
+				margin-bottom: 16rpx;
+
+				.icon-tile {
+					width: 24rpx;
+					height: 24rpx;
+					border-radius: 50%;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					margin-right: 10rpx;
+					box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
+				}
+
+				.metric-name {
+					font-size: 24rpx;
+					color: #8C9396;
+				}
+			}
+
+			.device-metric-value {
+				font-size: 44rpx;
+				font-weight: 700;
+				color: #2C3E50;
+				margin-bottom: 12rpx;
+			}
+
+			.device-metric-trend {
+				display: flex;
+				align-items: center;
+				font-size: 22rpx;
+
+				&.up {
+					color: #3BB44A;
+				}
+
+				&.down {
+					color: #FF5252;
+				}
+
+				text {
+					margin-left: 4rpx;
+				}
+			}
+		}
+	}
+
+	// 农业机械活动
+	.machinery-activity {
+		.machinery-metrics-grid {
+			display: grid;
+			grid-template-columns: repeat(2, 1fr);
+			gap: 16rpx;
+			padding: 16rpx;
+		}
+
+		.machinery-metric-card {
+			background: white;
+			border-radius: 16rpx;
+			padding: 20rpx;
+			box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.03);
+			border: 1px solid rgba(0, 0, 0, 0.02);
+
+			.machinery-metric-header {
+				display: flex;
+				align-items: center;
+				margin-bottom: 16rpx;
+
+				.icon-tile {
+					width: 24rpx;
+					height: 24rpx;
+					border-radius: 50%;
+					display: flex;
+					align-items: center;
+					justify-content: center;
+					margin-right: 10rpx;
+					box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
+				}
+
+				.metric-name {
+					font-size: 24rpx;
+					color: #8C9396;
+				}
+			}
+
+			.machinery-metric-value {
+				font-size: 44rpx;
+				font-weight: 700;
+				color: #2C3E50;
+				margin-bottom: 12rpx;
+			}
+
+			.machinery-metric-trend {
+				display: flex;
+				align-items: center;
+				font-size: 22rpx;
+
+				&.up {
+					color: #3BB44A;
+				}
+
+				&.down {
+					color: #FF5252;
+				}
+
+				text {
+					margin-left: 4rpx;
+				}
+			}
+
+			.machinery-metric-unit {
+				font-size: 22rpx;
+				color: #8C9396;
+			}
+		}
+	}
+
+	// 添加测试样式
+	.test-block {
+		background-color: #4CAF50;
+		padding: 30rpx;
+		margin: 20rpx;
+		border-radius: 12rpx;
+		display: flex;
+		justify-content: center;
+		align-items: center;
+	}
+
+	.test-text {
+		color: white;
+		font-size: 36rpx;
+		font-weight: bold;
+	}
+</style>

+ 168 - 171
pages/device/device-list/agricultural/index.vue

@@ -54,195 +54,192 @@
   </view>
 </template>
 
-<script>
-import { machinesDeviceList } from "@/api/services/agriculturalMachines.js";
-import storage from "@/utils/storage.js";
+<script setup>
+import { ref, reactive } from 'vue'
+import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
+import { machinesDeviceList } from "@/api/services/agriculturalMachines.js"
+import storage from "@/utils/storage.js"
 
-export default {
-  data() {
-    return {
-      machines: [],
-      loading: false,
-      pageNum: 1,
-      pageSize: 10,
-      total: 0,
-      allLoaded: false,
-      listHeight: 0,
-      filters: {
-        search: '',
-        machineType: '', // numeric or string code from backend
-        onlineStatus: '' // filter by onlineStatus
-      },
-      machineTypeDisplayOptions: ['全部','其他','拖拉机','收割机','播种机','喷雾机','农业智能体'],
-      machineTypeCodeOptions: ['', '0','1','2','3','4','5'],
-      onlineStatusDisplayOptions: ['全部','离线','在线','待命','作业中','维护中','故障'],
-      onlineStatusCodeOptions: ['', '0','1','2','3','4','5'],
-      selectedTypeLabel: '全部',
-      selectedStatusLabel: '全部'
-    }
-  },
+// Reactive data
+const machines = ref([])
+const loading = ref(false)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const allLoaded = ref(false)
+const listHeight = ref(0)
+const filters = reactive({
+  search: '',
+  machineType: '', // numeric or string code from backend
+  onlineStatus: '' // filter by onlineStatus
+})
+const machineTypeDisplayOptions = ref(['全部','其他','拖拉机','收割机','播种机','喷雾机','农业智能体'])
+const machineTypeCodeOptions = ref(['', '0','1','2','3','4','5'])
+const onlineStatusDisplayOptions = ref(['全部','离线','在线','待命','作业中','维护中','故障'])
+const onlineStatusCodeOptions = ref(['', '0','1','2','3','4','5'])
+const selectedTypeLabel = ref('全部')
+const selectedStatusLabel = ref('全部')
 
-  methods: {
-    formatDate(dateStr) {
-      if (!dateStr) return '-';
-      const d = new Date(dateStr);
-      if (isNaN(d.getTime())) return '-';
-      const y = d.getFullYear();
-      const m = (`0${d.getMonth() + 1}`).slice(-2);
-      const day = (`0${d.getDate()}`).slice(-2);
-      return `${y}-${m}-${day}`;
-    },
+// Methods
+const formatDate = (dateStr) => {
+  if (!dateStr) return '-'
+  const d = new Date(dateStr)
+  if (isNaN(d.getTime())) return '-'
+  const y = d.getFullYear()
+  const m = (`0${d.getMonth() + 1}`).slice(-2)
+  const day = (`0${d.getDate()}`).slice(-2)
+  return `${y}-${m}-${day}`
+}
 
-    getMachineTypeLabel(type) {
-      // backend uses numbers or strings; coerce to number if possible
-      const map = {
-        '0': '其他',
-        '1': '拖拉机',
-        '2': '收割机',
-        '3': '播种机',
-        '4': '喷雾机',
-        '5': '农业智能体'
-      };
-      return map[String(type)] || (type ? String(type) : '其他');
-    },
+const getMachineTypeLabel = (type) => {
+  // backend uses numbers or strings; coerce to number if possible
+  const map = {
+    '0': '其他',
+    '1': '拖拉机',
+    '2': '收割机',
+    '3': '播种机',
+    '4': '喷雾机',
+    '5': '农业智能体'
+  }
+  return map[String(type)] || (type ? String(type) : '其他')
+}
 
-    onlineStatusLabel(code) {
-      const map = {
-        '0': '离线',
-        '1': '在线',
-        '2': '待命',
-        '3': '作业中',
-        '4': '维护中',
-        '5': '故障'
-      };
-      return map[String(code)] || '-';
-    },
+const onlineStatusLabel = (code) => {
+  const map = {
+    '0': '离线',
+    '1': '在线',
+    '2': '待命',
+    '3': '作业中',
+    '4': '维护中',
+    '5': '故障'
+  }
+  return map[String(code)] || '-'
+}
 
-    onlineClass(code) {
-      const c = Number(code);
-      if (c === 1) return 'status-online';
-      if (c === 0) return 'status-offline';
-      if (c === 5) return 'status-error';
-      return 'status-idle';
-    },
+const onlineClass = (code) => {
+  const c = Number(code)
+  if (c === 1) return 'status-online'
+  if (c === 0) return 'status-offline'
+  if (c === 5) return 'status-error'
+  return 'status-idle'
+}
 
-    maintenanceLabel(code) {
-      const map = { '1': '正常', '2': '需保养', '3': '待修复' };
-      return map[code] || (code ? code : '-');
-    },
+const maintenanceLabel = (code) => {
+  const map = { '1': '正常', '2': '需保养', '3': '待修复' }
+  return map[code] || (code ? code : '-')
+}
 
-    locationLabel(code) {
-      const map = { '1': '良好', '2': '异常', '3': '无信号' };
-      return map[code] || (code ? code : '-');
-    },
+const locationLabel = (code) => {
+  const map = { '1': '良好', '2': '异常', '3': '无信号' }
+  return map[code] || (code ? code : '-')
+}
 
-    onTypeChange(e) {
-      const idx = Number(e.detail.value);
-      this.selectedTypeLabel = this.machineTypeDisplayOptions[idx];
-      this.filters.machineType = this.machineTypeCodeOptions[idx] || '';
-    },
+const onTypeChange = (e) => {
+  const idx = Number(e.detail.value)
+  selectedTypeLabel.value = machineTypeDisplayOptions.value[idx]
+  filters.machineType = machineTypeCodeOptions.value[idx] || ''
+}
 
-    onStatusChange(e) {
-      const idx = Number(e.detail.value);
-      this.selectedStatusLabel = this.onlineStatusDisplayOptions[idx];
-      this.filters.onlineStatus = this.onlineStatusCodeOptions[idx] || '';
-    },
+const onStatusChange = (e) => {
+  const idx = Number(e.detail.value)
+  selectedStatusLabel.value = onlineStatusDisplayOptions.value[idx]
+  filters.onlineStatus = onlineStatusCodeOptions.value[idx] || ''
+}
 
-    onSearch() {
-      this.pageNum = 1;
-      this.machines = [];
-      this.allLoaded = false;
-      this.fetchMachines();
-    },
+const onSearch = () => {
+  pageNum.value = 1
+  machines.value = []
+  allLoaded.value = false
+  fetchMachines()
+}
 
-    fetchMachines() {
-      if (this.loading || this.allLoaded) return;
-      this.loading = true;
-      uni.showLoading({ title: '加载中...' });
+const fetchMachines = () => {
+  if (loading.value || allLoaded.value) return
+  loading.value = true
+  uni.showLoading({ title: '加载中...' })
 
-      const params = {
-        pageNum: this.pageNum,
-        pageSize: this.pageSize,
-        machineName: this.filters.search || undefined,
-        machineCode: this.filters.search || undefined,
-        deptName: this.filters.search || undefined,
-        machineType: this.filters.machineType || undefined,
-        onlineStatus: this.filters.onlineStatus || undefined
-      };
+  const params = {
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+    machineName: filters.search || undefined,
+    machineCode: filters.search || undefined,
+    deptName: filters.search || undefined,
+    machineType: filters.machineType || undefined,
+    onlineStatus: filters.onlineStatus || undefined
+  }
 
-      machinesDeviceList(params)
-        .then(res => {
-          if (res && res.data.code === 200 && res.data.rows) {
-            const data = res.data.rows;
-            if (this.pageNum === 1) {
-              this.machines = data;
-            } else {
-              this.machines = this.machines.concat(data);
-            }
-            this.total = res.data.total
-            if (this.machines.length >= this.total) {
-              this.allLoaded = true;
-            } else {
-              this.allLoaded = false;
-            }
-          } else {
-            uni.showToast({ title: (res && res.data.rows && res.data.msg) || '获取数据失败', icon: 'none' });
-          }
-        })
-        .catch(err => {
-          console.error('fetchMachines error', err);
-          uni.showToast({ title: '请求失败', icon: 'none' });
-        })
-        .finally(() => {
-          this.loading = false;
-          uni.hideLoading();
-        });
-    },
+  machinesDeviceList(params)
+    .then(res => {
+      if (res && res.data.code === 200 && res.data.rows) {
+        const data = res.data.rows
+        if (pageNum.value === 1) {
+          machines.value = data
+        } else {
+          machines.value = machines.value.concat(data)
+        }
+        total.value = res.data.total
+        if (machines.value.length >= total.value) {
+          allLoaded.value = true
+        } else {
+          allLoaded.value = false
+        }
+      } else {
+        uni.showToast({ title: (res && res.data.rows && res.data.msg) || '获取数据失败', icon: 'none' })
+      }
+    })
+    .catch(err => {
+      console.error('fetchMachines error', err)
+      uni.showToast({ title: '请求失败', icon: 'none' })
+    })
+    .finally(() => {
+      loading.value = false
+      uni.hideLoading()
+    })
+}
 
-    loadMore() {
-      if (this.loading || this.allLoaded) return;
-      this.pageNum += 1;
-      this.fetchMachines();
-    },
+const loadMore = () => {
+  if (loading.value || allLoaded.value) return
+  pageNum.value += 1
+  fetchMachines()
+}
 
-    navigateToDetail(machine) {
-      // Placeholder navigation - create detail page separately if needed
-      uni.navigateTo({
-        url: '/pages/device/device-list/detail-machine?id=' + machine.id,
-        success: (res) => {
-          setTimeout(() => {
-            uni.$emit('agriculturalMachinesData', {
-              machineId: machine.id,
-              machineCode: machine.machineCode,
-              machineName: machine.machineName,
-              onlineStatus: machine.onlineStatus,
-              updateTime: machine.updateTime
-            });
-          }, 100);
-        }
-      });
+const navigateToDetail = (machine) => {
+  // Placeholder navigation - create detail page separately if needed
+  uni.navigateTo({
+    url: '/pages/device/device-list/detail-machine?id=' + machine.id,
+    success: (res) => {
+      setTimeout(() => {
+        uni.$emit('agriculturalMachinesData', {
+          machineId: machine.id,
+          machineCode: machine.machineCode,
+          machineName: machine.machineName,
+          onlineStatus: machine.onlineStatus,
+          updateTime: machine.updateTime
+        })
+      }, 100)
     }
-  },
+  })
+}
 
-  onLoad() {
-    // compute list height to make scroll-view fill screen (simple heuristic)
-    const systemInfo = uni.getSystemInfoSync();
-    // subtract header/search height approx 160px
-    this.listHeight = systemInfo.windowHeight - 160;
-    // initial load
-    this.fetchMachines();
-  },
+// Lifecycle hooks
+onLoad(() => {
+  // compute list height to make scroll-view fill screen (simple heuristic)
+  const systemInfo = uni.getSystemInfoSync()
+  // subtract header/search height approx 160px
+  listHeight.value = systemInfo.windowHeight - 160
+  // initial load
+  fetchMachines()
+})
 
-  onPullDownRefresh() {
-    this.pageNum = 1;
-    this.machines = [];
-    this.allLoaded = false;
-    this.fetchMachines();
-    setTimeout(() => {
-      uni.stopPullDownRefresh();
-    }, 800);
-  }
-}
+onPullDownRefresh(() => {
+  pageNum.value = 1
+  machines.value = []
+  allLoaded.value = false
+  fetchMachines()
+  setTimeout(() => {
+    uni.stopPullDownRefresh()
+  }, 800)
+})
 </script>
 
 <style scoped>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 630 - 797
pages/device/device-list/detail-camera.vue


+ 193 - 258
pages/device/device-list/detail-collector.vue

@@ -324,273 +324,208 @@
   </view>
 </template>
 
-<script>
-import { getDeviceCollectorDetail } from "@/api/services/device.js";
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { getDeviceCollectorDetail } from "@/api/services/device.js"
 import { formatSmartTime, formatDate } from '@/utils/dateUtils'
-export default {
-  data() {
-    return {
-      deviceInfo: {
-        deviceId: '',
-        name: '设备加载中...',
-        status: '',
-        location: '正在获取位置...',
-        lastUpdate: '',
-        deviceType: 'weather' ,// 默认类型,会根据API返回更新
-		deviceTypeId:null
-      },
-      
-      // 气象站数据
-      weatherData: {
-        temperature: '',
-        humidity: '',
-        rainfall: '',
-        windDirection: '',
-        windSpeed: '',
-        pressure: '',
-        illumination: '',
-        updateTime: ''
-      },
-      
-      // 土壤墒情数据
-      soilData: {
-        temperature: '',
-        moisture: '',
-        nitrogen: '',
-        phosphorus: '',
-        potassium: '',
-        conductivity: '',
-        ph: '',
-        updateTime: ''
-      },
-      
-      // 告警数据
-      alertHistory: [],
-      
-      // 刷新状态
-      isRefreshing: false,
-      
-      // 数值更新动画
-      updatedFields: {
-        weather: {},
-        soil: {}
-      },
-    }
-  },
-  
-  computed: {
-    // 获取所有未处理的告警
-    getUnhandledAlerts() {
-      return this.alertHistory.filter(alert => alert.status === 0);//未处理
-    }
-  },
-  
-  onReady() {
-    // 设置页面标题
-    uni.setNavigationBarTitle({
-      title: this.deviceInfo.name
-    })
-  },
+
+// 响应式数据
+const deviceInfo = reactive({
+  deviceId: '',
+  name: '设备加载中...',
+  status: '',
+  location: '正在获取位置...',
+  lastUpdate: '',
+  deviceType: 'weather',
+  deviceTypeId: null
+})
+
+// 气象站数据
+const weatherData = reactive({
+  temperature: '',
+  humidity: '',
+  rainfall: '',
+  windDirection: '',
+  windSpeed: '',
+  pressure: '',
+  illumination: '',
+  updateTime: ''
+})
+
+// 土壤墒情数据
+const soilData = reactive({
+  temperature: '',
+  moisture: '',
+  nitrogen: '',
+  phosphorus: '',
+  potassium: '',
+  conductivity: '',
+  ph: '',
+  updateTime: ''
+})
+
+// 告警数据
+const alertHistory = ref([])
+
+// 刷新状态
+const isRefreshing = ref(false)
+
+// 数值更新动画
+const updatedFields = reactive({
+  weather: {},
+  soil: {}
+})
+
+// 计算属性
+// 获取所有未处理的告警
+const getUnhandledAlerts = computed(() => {
+  return alertHistory.value.filter(alert => alert.status === 0)
+})
+
+// 方法
+// 获取设备采集器详情
+const fetchDeviceCollectorDetail = () => {
+  if (!deviceInfo.deviceId) return
   
-  onLoad(options) {
-	  uni.$once('passDeviceData', (data) => {
-	      console.log('接收到数据', data);
-		  // 如果有传入设备ID,则获取设备信息
-		  if (data && data.deviceId) {
-		    this.deviceInfo.deviceId = data.deviceId;
-		    this.deviceInfo.location = data.fieldName;
-		    this.deviceInfo.name = data.deviceName;
-		    this.deviceInfo.status = data.status;
-		    this.deviceInfo.deviceTypeId = data.deviceTypeId || '';
-		    
-		    // 加载设备详情
-		    this.fetchDeviceCollectorDetail();
-		  }
-	    });
-    
-  },
+  uni.showLoading({
+    title: '加载数据中...'
+  })
   
-  methods: {
-	formatDate,
-    formatSmartTime,
-    // 获取设备采集器详情
-    fetchDeviceCollectorDetail() {
-      if (!this.deviceInfo.deviceId) return;
-      
-      uni.showLoading({
-        title: '加载数据中...'
-      });
-      
-      getDeviceCollectorDetail(this.deviceInfo.deviceId)
-        .then(res => {
-          console.log('设备详情数据:', res);
-          if (res.data.data && res.data.code === 200) {
-            const detail = res.data.data;
-            this.deviceInfo.lastUpdate = detail.collectTime
-            // 保存旧数据用于比较
-            let oldData = null;
-            
-            // 根据设备类型更新数据
-            if (this.deviceInfo.deviceTypeId === '4') { //气象设备
-				if(this.weatherData){
-					oldData = JSON.parse(JSON.stringify(this.weatherData));
-				}
-              // 更新气象数据
-              this.weatherData = {
-                temperature: detail.temperature || '',
-                humidity: detail.humidity || '',
-                rainfall: detail.rainfall || '',
-                windDirection: detail.windDirection || '',
-                windSpeed: detail.windSpeed || '',
-                pressure: detail.pressure || '',
-                illumination: detail.illumination || '',
-                updateTime: detail.collectTime|| '暂无数据'
-              };
-              
-              // 检查哪些字段发生了变化
-              this.updatedFields.weather = {};
-              Object.keys(this.weatherData).forEach(key => {
-                if (key !== 'updateTime' && this.weatherData[key] !== oldData[key]) {
-                  this.updatedFields.weather[key] = true;
-                  
-                  // 1秒后清除动画标记
-                  setTimeout(() => {
-                    this.$set(this.updatedFields.weather, key, false);
-                  }, 1000);
-                }
-              });
-              
-            } else if (this.deviceInfo.deviceTypeId === '1') { // 土壤设备
-			if(this.soilData){
-				oldData = JSON.parse(JSON.stringify(this.soilData));
-			}
-              
-              // 更新土壤数据
-              this.soilData = {
-                temperature: detail.temperature || '',
-                moisture: detail.soilTemperature || '',
-                nitrogen: detail.soilN || '',
-                phosphorus: detail.soilP || '',
-                potassium: detail.soilK || '',
-                conductivity: detail.conductivity || '',
-                ph: detail.ph || '',
-                updateTime: detail.collectTime || '暂无数据'
-              };
+  getDeviceCollectorDetail(deviceInfo.deviceId)
+    .then(res => {
+      console.log('设备详情数据:', res)
+      if (res.data.data && res.data.code === 200) {
+        const detail = res.data.data
+        deviceInfo.lastUpdate = detail.collectTime
+        // 保存旧数据用于比较
+        let oldData = null
+        
+        // 根据设备类型更新数据
+        if (deviceInfo.deviceTypeId === '4') { //气象设备
+          if (weatherData.temperature) {
+            oldData = JSON.parse(JSON.stringify(weatherData))
+          }
+          // 更新气象数据
+          Object.assign(weatherData, {
+            temperature: detail.temperature || '',
+            humidity: detail.humidity || '',
+            rainfall: detail.rainfall || '',
+            windDirection: detail.windDirection || '',
+            windSpeed: detail.windSpeed || '',
+            pressure: detail.pressure || '',
+            illumination: detail.illumination || '',
+            updateTime: detail.collectTime|| '暂无数据'
+          })
+          
+          // 检查哪些字段发生了变化
+          updatedFields.weather = {}
+          Object.keys(weatherData).forEach(key => {
+            if (key !== 'updateTime' && oldData && weatherData[key] !== oldData[key]) {
+              updatedFields.weather[key] = true
               
-              // 检查哪些字段发生了变化
-              this.updatedFields.soil = {};
-              Object.keys(this.soilData).forEach(key => {
-                if (key !== 'updateTime' && this.soilData[key] !== oldData[key]) {
-                  this.updatedFields.soil[key] = true;
-                  
-                  // 1秒后清除动画标记
-                  setTimeout(() => {
-                    this.$set(this.updatedFields.soil, key, false);
-                  }, 1000);
-                }
-              });
+              // 1秒后清除动画标记
+              setTimeout(() => {
+                updatedFields.weather[key] = false
+              }, 1000)
             }
-            
-            // 更新告警信息
-            if (detail.alertRecordList && detail.alertRecordList.length > 0) {
-              this.alertHistory = detail.alertRecordList.map(alert => ({
-                id: alert.alertId,
-                time: alert.alertTime,
-                type: alert.alertContent,
-                status: alert.processStatus,
-                level: alert.alertLevel
-              }));
-            }
-            
-            // 更新页面标题
-            uni.setNavigationBarTitle({
-              title: this.deviceInfo.name
-            });
-          } else {
-            uni.showToast({
-              title: '暂无设备信息',
-              icon: 'none'
-            });
+          })
+          
+        } else if (deviceInfo.deviceTypeId === '1') { // 土壤设备
+          if (soilData.temperature) {
+            oldData = JSON.parse(JSON.stringify(soilData))
           }
+          
+          // 更新土壤数据
+          Object.assign(soilData, {
+            temperature: detail.temperature || '',
+            moisture: detail.soilTemperature || '',
+            nitrogen: detail.soilN || '',
+            phosphorus: detail.soilP || '',
+            potassium: detail.soilK || '',
+            conductivity: detail.conductivity || '',
+            ph: detail.ph || '',
+            updateTime: detail.collectTime || '暂无数据'
+          })
+          
+          // 检查哪些字段发生了变化
+          updatedFields.soil = {}
+          Object.keys(soilData).forEach(key => {
+            if (key !== 'updateTime' && oldData && soilData[key] !== oldData[key]) {
+              updatedFields.soil[key] = true
+              
+              // 1秒后清除动画标记
+              setTimeout(() => {
+                updatedFields.soil[key] = false
+              }, 1000)
+            }
+          })
+        }
+        
+        // 更新告警信息
+        if (detail.alertRecordList && detail.alertRecordList.length > 0) {
+          alertHistory.value = detail.alertRecordList.map(alert => ({
+            id: alert.alertId,
+            time: alert.alertTime,
+            type: alert.alertContent,
+            status: alert.processStatus,
+            level: alert.alertLevel
+          }))
+        }
+        
+        // 更新页面标题
+        uni.setNavigationBarTitle({
+          title: deviceInfo.name
         })
-        .catch(error => {
-          console.error('获取设备详情失败', error);
-          uni.showToast({
-            title: '获取设备数据失败',
-            icon: 'none'
-          });
+      } else {
+        uni.showToast({
+          title: '暂无设备信息',
+          icon: 'none'
         })
-        .finally(() => {
-          uni.hideLoading();
-          this.isRefreshing = false;
-        });
-    },
-	// 格式还日期格式;返回 今天:18:00
-    // formatSmartTime(timeStr) {
-    //   if (!timeStr) return '未知';
-    
-    //   // iOS兼容:将 "2025-06-19 09:15:00" 转换为 "2025-06-19T09:15:00"
-    //   const safeStr = timeStr.replace(' ', 'T');
-    
-    //   const inputDate = new Date(safeStr);
-    //   if (isNaN(inputDate.getTime())) return '时间格式错误';
-    
-    //   const now = new Date();
-    
-    //   // 取日期差值(单位:天)
-    //   const inputDayStart = new Date(inputDate.getFullYear(), inputDate.getMonth(), inputDate.getDate());
-    //   const nowDayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
-    
-    //   const diffTime = nowDayStart - inputDayStart;
-    //   const oneDay = 1000 * 60 * 60 * 24;
-    
-    //   const timePart = inputDate.toTimeString().slice(0, 5); // HH:mm
+      }
+    })
+    .catch(error => {
+      console.error('获取设备详情失败', error)
+      uni.showToast({
+        title: '获取设备数据失败',
+        icon: 'none'
+      })
+    })
+    .finally(() => {
+      uni.hideLoading()
+      isRefreshing.value = false
+    })
+}
+
+// 刷新数据
+const refreshData = () => {
+  if (isRefreshing.value) return
+  
+  isRefreshing.value = true
+  fetchDeviceCollectorDetail()
+}
+
+// 生命周期钩子
+onMounted(() => {
+  uni.setNavigationBarTitle({
+    title: deviceInfo.name
+  })
+})
+
+// uni-app 生命周期
+uni.$once('passDeviceData', (data) => {
+  console.log('接收到数据', data)
+  // 如果有传入设备ID,则获取设备信息
+  if (data && data.deviceId) {
+    deviceInfo.deviceId = data.deviceId
+    deviceInfo.location = data.fieldName
+    deviceInfo.name = data.deviceName
+    deviceInfo.status = data.status
+    deviceInfo.deviceTypeId = data.deviceTypeId || ''
     
-    //   if (diffTime === 0) {
-    //     return `今天 ${timePart}`;
-    //   } else if (diffTime === oneDay) {
-    //     return `昨天 ${timePart}`;
-    //   } else if (diffTime === oneDay * 2) {
-    //     return `前天 ${timePart}`;
-    //   } else {
-    //     return `${inputDate.getMonth() + 1}月${inputDate.getDate()}日 ${timePart}`;
-    //   }
-    // },
-
-    // 刷新数据
-    refreshData() {
-      if (this.isRefreshing) return;
-      
-      this.isRefreshing = true;
-      this.fetchDeviceCollectorDetail();
-    },
-	// 格式化日期
-	// formatDate(dateStr) {
-	//   if (!dateStr) return '未知';
-	  
-	//   // 解析为 Date
-	//   const parsedStr = dateStr.replace(' ', 'T'); 
-	//   const date = new Date(parsedStr);
-	//   if (isNaN(date)) return '无效时间';
-	  
-	//   const now = new Date();
-	
-	//   // 计算差时长(分钟)
-	//   const diff = Math.floor((now - date) / 1000 / 60);
-	  
-	//   if (diff < 1) return '刚刚更新';
-	//   if (diff < 5) return '1分钟前更新';
-	//   if (diff < 10) return '5分钟前更新';
-	//   if (diff < 60) return `${diff}分钟前更新`;
-	  
-	//   if (diff < 120) return '1小时前更新';
-	//   if (diff < 24 * 60) return `${Math.floor(diff / 60)}小时前更新`;
-	//   if (diff < 7 * 24 * 60) return `${Math.floor(diff / (60 * 24))}天前更新`;
-	  
-	//   return parsedStr.split('T')[0] + ' 更新';
-	// },
+    // 加载设备详情
+    fetchDeviceCollectorDetail()
   }
-}
+})
 </script>
 
 <style>
@@ -1075,4 +1010,4 @@ export default {
   margin-left: 16rpx;
   font-weight: 600;
 }
-</style> 
+</style>

+ 450 - 512
pages/device/device-list/detail-machine.vue

@@ -338,538 +338,476 @@
   </view>
 </template>
 
-<script>
-import { machineAlarmRecordsList } from '@/api/services/machineAlarmRecords';
-import { deviceTasksList, startTask, deleteTask } from '@/api/services/job';
-export default {
-  data() {
-    return {
-      // 设备信息
-      deviceInfo: {
-        // id: '',
-        // name: '智能播种机-001',
-        // deviceId: 'AGM-001-2024',
-        // location: '东区水稻田A块',
-        // status: 'online', // online, offline
-        // workStatus: 'idle', // working, idle, error
-        // battery: 85,
-        // gpsSignal: 92,
-        // workDuration: '2.5',
-        // workArea: '15.6',
-        // currentSpeed: '0',
-        // lastUpdate: '1分钟前',
-        // statusUpdateTime: '1分钟前',
-        // batteryUpdateTime: '30秒前',
-        // gpsUpdateTime: '15秒前',
-        // speedUpdateTime: '实时'
-      },
-
-      // 作业任务列表
-      taskList: [],
-      selectedTaskId: null,
-      
-      // 控制状态
-      isEngineOn: false,
-      activeControl: '', // forward, backward, left, right
-      currentSpeed: 0,
-      
-      // 页面状态
-      isRefreshing: false,
-      alarmTypeMap: {
-        0: '其他',
-        1: '发动机',
-        2: '燃油',
-        3: '温度',
-        4: '压力',
-        5: '定位',
-      },
-      // 告警数据
-      alerts: [
-        // {
-        //   id: 1,
-        //   title: '电量不足警告',
-        //   description: '设备电量低于30%,建议及时充电',
-        //   level: 'medium',
-        //   time: '5分钟前',
-        //   handled: false
-        // },
-        // {
-        //   id: 2,
-        //   title: 'GPS信号弱',
-        //   description: '当前GPS信号较弱,可能影响导航精度',
-        //   level: 'low',
-        //   time: '10分钟前',
-        //   handled: false
-        // },
-        // {
-        //   id: 3,
-        //   title: '作业异常',
-        //   description: '检测到播种深度异常,请检查设备状态',
-        //   level: 'high',
-        //   time: '15分钟前',
-        //   handled: false
-        // }
-      ]
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { machineAlarmRecordsList } from '@/api/services/machineAlarmRecords'
+import { deviceTasksList, startTask, deleteTask } from '@/api/services/job'
+
+// 响应式数据
+const deviceInfo = reactive({
+  id: '',
+  machineCode: '',
+  machineName: '',
+  onlineStatus: 0,
+  updateTime: '',
+  location: ''
+})
+
+// 作业任务列表
+const taskList = ref([])
+const selectedTaskId = ref(null)
+
+// 控制状态
+const isEngineOn = ref(false)
+const activeControl = ref('') // forward, backward, left, right
+const currentSpeed = ref(0)
+
+// 页面状态
+const isRefreshing = ref(false)
+const alarmTypeMap = {
+  0: '其他',
+  1: '发动机',
+  2: '燃油',
+  3: '温度',
+  4: '压力',
+  5: '定位',
+}
+// 告警数据
+const alerts = ref([])
+
+// 计算属性
+// 未处理的告警
+const getUnhandledAlerts = computed(() => {
+  return alerts.value.filter(alert => !alert.handled)
+})
+
+// 方法
+// 加载设备告警数据
+const loadDeviceAlarmData = () => {
+  return machineAlarmRecordsList({
+    machineId: deviceInfo.id
+  }).then(res => {
+    console.log('设备告警数据:', res)
+    if (res.data.code === 200 && res.data.rows) {
+      alerts.value = res.data.rows.map(item => ({
+        id: item.id,
+        title: item.alarmDesc,
+        level: item.alarmLevel === 3 ? 'high' : item.alarmLevel === 2 ? 'medium' : 'low',
+        time: item.alarmTime
+      }))
     }
-  },
+  })
+}
+
+const formatAlarmType = (level) => {
+  return alarmTypeMap[level] || '其他'
+}
+
+// 加载设备数据
+const loadDeviceData = () => {
+  console.log('加载设备数据:', deviceInfo.deviceId)
+}
+
+// 加载当前设备的作业任务列表
+const loadTaskList = () => {
+  const params = {
+    pageNum: 1,
+    pageSize: 10,
+    deviceId: deviceInfo.id
+  }
+
+  return deviceTasksList(params)
+    .then((res) => {
+      console.log("res任务作业列表",res)
+      
+      const { data } = res || {}
+      if (data && data.code === 200) {
+        const list = (data.rows) ? data.rows : []
+
+        const areaTypeTextMap = {
+          1: '回字形',
+          2: '弓字形',
+          3: '自定义',
+          4: '垄沟'
+        }
+
+        taskList.value = list.map((item) => ({
+          id: item.id,
+          name: item.taskName,
+          fieldArea: item.workArea ? item.workArea.areaName : '',
+          planTime: item.createTime,
+          status: item.taskStatus,
+          statusText: item.taskStatusDesc,
+          areaType: item.workAreas && item.workAreas.areaType,
+          areaTypeText: areaTypeTextMap[item.workAreas && item.workAreas.areaType || 0] || '未知'
+        }))
+        console.log('taskList',taskList.value)
+
+        if (taskList.value.length) {
+          const exists = selectedTaskId.value && taskList.value.some(t => t.id === selectedTaskId.value)
+          selectedTaskId.value = exists ? selectedTaskId.value : taskList.value[0].id
+        } else {
+          selectedTaskId.value = null
+        }
+      } else {
+        taskList.value = []
+        selectedTaskId.value = null
+        uni.showToast({
+          title: (data && data.msg) ? data.msg : '获取作业任务失败',
+          icon: 'none'
+        })
+      }
+    })
+    .catch((err) => {
+      console.error('获取作业任务列表失败:', err)
+      taskList.value = []
+      selectedTaskId.value = null
+      uni.showToast({
+        title: '获取作业任务失败',
+        icon: 'none'
+      })
+    })
+}
+
+// 刷新数据
+const refreshData = () => {
+  isRefreshing.value = true
+
+  Promise.all([
+    loadTaskList(),
+    loadDeviceAlarmData()
+  ]).finally(() => {
+    isRefreshing.value = false
+    deviceInfo.lastUpdate = '刚刚'
+
+    uni.showToast({
+      title: '刷新成功',
+      icon: 'success',
+      duration: 1500
+    })
+  })
+}
+
+// 获取工作状态文本
+const getWorkStatusText = (status) => {
+  const statusMap = {
+    working: '工作中',
+    idle: '空闲',
+    error: '故障'
+  }
+  return statusMap[status] || '未知'
+}
+
+// 获取GPS信号文本
+const getGpsSignalText = (signal) => {
+  if (signal >= 80) return '强'
+  if (signal >= 60) return '中等'
+  if (signal >= 40) return '弱'
+  return '无信号'
+}
+
+// 切换引擎状态
+const toggleEngine = () => {
+  if (deviceInfo.status === 'offline') {
+    uni.showToast({
+      title: '设备离线,无法操作',
+      icon: 'none'
+    })
+    return
+  }
   
-  computed: {
-    // 未处理的告警
-    getUnhandledAlerts() {
-      return this.alerts.filter(alert => !alert.handled);
-    }
-  },
+  isEngineOn.value = !isEngineOn.value
   
-  onLoad(options) {
-    this.deviceInfo.id = options.id;
-    // 获取设备ID参数
-    uni.$once('agriculturalMachinesData', (data) => {
-      if (data ) {
-        this.deviceInfo.machineCode = data.machineCode;
-        this.deviceInfo.machineName = data.machineName;
-        this.deviceInfo.onlineStatus = data.onlineStatus;
-        this.deviceInfo.updateTime = data.updateTime;
-      }
-    });
-    
-    // 设置导航栏标题
-    uni.setNavigationBarTitle({
-      title: '农机设备详情'
-    });
-    
-    // 加载设备数据
-    this.loadDeviceData();
-    // 默认加载当前设备的作业任务
-    this.loadTaskList();
-    // 加载设备告警数据
-    this.loadDeviceAlarmData();
-  },
+  if (isEngineOn.value) {
+    deviceInfo.workStatus = 'idle'
+    uni.showToast({
+      title: '引擎已启动',
+      icon: 'success'
+    })
+  } else {
+    deviceInfo.workStatus = 'idle'
+    currentSpeed.value = 0
+    stopAllControls()
+    uni.showToast({
+      title: '引擎已关闭',
+      icon: 'success'
+    })
+  }
+}
+
+// 开始控制
+const startControl = (direction) => {
+  if (!isEngineOn.value || deviceInfo.status === 'offline') {
+    return
+  }
   
-  onShow() {
-    // 页面显示时刷新数据
-    this.refreshData();
-  },
+  activeControl.value = direction
+  deviceInfo.workStatus = 'working'
   
-  onHide() {
-    // 页面隐藏时停止所有控制操作
-    this.stopAllControls();
-  },
+  switch (direction) {
+    case 'forward':
+      currentSpeed.value = 5
+      break
+    case 'backward':
+      currentSpeed.value = 2
+      break
+    case 'left':
+    case 'right':
+      currentSpeed.value = 3
+      break
+  }
   
-  methods: {
-    // 加载设备告警数据
-    loadDeviceAlarmData() {
-      return machineAlarmRecordsList({
-        machineId: this.deviceInfo.id
-      }).then(res => {
-        console.log('设备告警数据:', res);
-        if (res.data.code === 200 && res.data.rows) {
-          this.alerts = res.data.rows.map(item => ({
-            id: item.id,
-            // title: this.formatAlarmType(item.alarmType),
-            title: item.alarmDesc,
-            level: item.alarmLevel === 3 ? 'high' : item.alarmLevel === 2 ? 'medium' : 'low',
-            time: item.alarmTime
-          }));
-        }
-      });
-    },
-    formatAlarmType(level) {
-      return this.alarmTypeMap[level] || '其他';
-    },
-    // 加载设备数据
-    loadDeviceData() {
-      // 这里可以调用API获取设备详细信息
-      console.log('加载设备数据:', this.deviceInfo.deviceId);
-    },
-
-    // 加载当前设备的作业任务列表
-    loadTaskList() {
-      const params = {
-        pageNum: 1,
-        pageSize: 10,
-        // 文档未声明 deviceId 过滤,但为了兼容后端如支持按设备筛选,这里传入
-        deviceId: this.deviceInfo.id
-      };
-
-      return deviceTasksList(params)
-        .then((res) => {
-          console.log("res任务作业列表",res);
-          
-          const { data } = res || {};
+  sendControlCommand(direction, true)
+}
+
+// 停止控制
+const stopControl = () => {
+  if (activeControl.value) {
+    sendControlCommand(activeControl.value, false)
+  }
+  
+  activeControl.value = ''
+  currentSpeed.value = 0
+  deviceInfo.workStatus = 'idle'
+}
+
+// 停止所有控制
+const stopAllControls = () => {
+  activeControl.value = ''
+  currentSpeed.value = 0
+  if (isEngineOn.value) {
+    deviceInfo.workStatus = 'idle'
+  }
+}
+
+// 发送控制指令
+const sendControlCommand = (direction, isStart) => {
+  console.log(`发送控制指令: ${direction}, 开始: ${isStart}`)
+}
+
+// 紧急停止
+const emergencyStop = () => {
+  if (deviceInfo.status === 'offline') {
+    uni.showToast({
+      title: '设备离线,无法操作',
+      icon: 'none'
+    })
+    return
+  }
+  
+  uni.showModal({
+    title: '紧急停止',
+    content: '确定要执行紧急停止吗?设备将立即停止所有操作。',
+    success: (res) => {
+      if (res.confirm) {
+        isEngineOn.value = false
+        stopAllControls()
+        
+        uni.showToast({
+          title: '紧急停止已执行',
+          icon: 'success'
+        })
+      }
+    }
+  })
+}
+
+// 返回充电
+const returnHome = () => {
+  if (deviceInfo.status === 'offline') {
+    uni.showToast({
+      title: '设备离线,无法操作',
+      icon: 'none'
+    })
+    return
+  }
+  
+  uni.showToast({
+    title: '设备正在返回充电站',
+    icon: 'success'
+  })
+}
+
+// 自动模式
+const autoMode = () => {
+  if (deviceInfo.status === 'offline') {
+    uni.showToast({
+      title: '设备离线,无法操作',
+      icon: 'none'
+    })
+    return
+  }
+  
+  uni.showToast({
+    title: '已切换到自动模式',
+    icon: 'success'
+  })
+}
+
+// 跳转到新增作业页面
+const goToCreateJob = () => {
+  uni.navigateTo({
+    url: `/pages/device/job-create/index?machineCode=${deviceInfo.machineCode}&id=${deviceInfo.id}`
+  })
+}
+
+// 选择作业任务
+const selectTask = (task) => {
+  selectedTaskId.value = task.id
+}
+
+// 开始作业
+const startWork = () => {
+  if (!taskList.value.length) {
+    uni.showToast({
+      title: '暂无可执行的作业任务',
+      icon: 'none'
+    })
+    return
+  }
+  if (!selectedTaskId.value) {
+    uni.showToast({
+      title: '请先选择一个作业任务',
+      icon: 'none'
+    })
+    return
+  }
+  if (deviceInfo.onlineStatus !== 1) {
+    uni.showToast({
+      title: '设备未在线,无法开始作业',
+      icon: 'none'
+    })
+    return
+  }
+
+  const task = taskList.value.find(t => t.id === selectedTaskId.value)
+  uni.showModal({
+    title: '开始作业',
+    content: `确定开始执行「${task.name}」吗?`,
+    success: async (res) => {
+      if (res.confirm) {
+        try {
+          uni.showLoading({ title: '启动中...' })
+          const resp = await startTask(selectedTaskId.value)
+          const { data } = resp || {}
           if (data && data.code === 200) {
-            const list = (data.rows) ? data.rows : [];
-
-            // 适配页面字段
-            const areaTypeTextMap = {
-              1: '回字形',
-              2: '弓字形',
-              3: '自定义',
-              4: '垄沟'
-            }
-
-            this.taskList = list.map((item) => ({
-              id: item.id,
-              name: item.taskName,
-              fieldArea: item.workArea ? item.workArea.areaName : '',
-              planTime: item.createTime,
-              status: item.taskStatus,
-              statusText: item.taskStatusDesc,
-              areaType: item.workAreas && item.workAreas.areaType,
-              areaTypeText: areaTypeTextMap[item.workAreas && item.workAreas.areaType || 0] || '未知'
-            }));
-            console.log('taskList',this.taskList);
-
-            // 默认选中第一条
-            if (this.taskList.length) {
-              // 若当前已选中且仍存在,则保持;否则选第一条
-              const exists = this.selectedTaskId && this.taskList.some(t => t.id === this.selectedTaskId);
-              this.selectedTaskId = exists ? this.selectedTaskId : this.taskList[0].id;
-            } else {
-              this.selectedTaskId = null;
-            }
+            uni.showToast({
+              title: '作业已启动',
+              icon: 'success'
+            })
+            isEngineOn.value = true
+            deviceInfo.workStatus = 'working'
+
+            setTimeout(() => {
+              uni.navigateTo({
+                url: `/pages/device/job-detail/index?id=${selectedTaskId.value}&deviceId=${deviceInfo.machineCode}&deviceName=${deviceInfo.machineName}`
+              })
+            }, 300)
           } else {
-            this.taskList = [];
-            this.selectedTaskId = null;
             uni.showToast({
-              title: (data && data.msg) ? data.msg : '获取作业任务失败',
+              title: (data && data.msg) ? data.msg : '启动失败',
               icon: 'none'
-            });
+            })
           }
-        })
-        .catch((err) => {
-          console.error('获取作业任务列表失败:', err);
-          this.taskList = [];
-          this.selectedTaskId = null;
+        } catch (e) {
+          console.error('开始作业失败:', e)
           uni.showToast({
-            title: '获取作业任务失败',
+            title: '网络异常,启动失败',
             icon: 'none'
-          });
-        });
-    },
-    
-    // 刷新数据
-    refreshData() {
-      this.isRefreshing = true;
-
-      Promise.all([
-        this.loadTaskList(),
-        this.loadDeviceAlarmData()
-      ]).finally(() => {
-        this.isRefreshing = false;
-        // 更新最近更新时间
-        this.deviceInfo.lastUpdate = '刚刚';
-
-        uni.showToast({
-          title: '刷新成功',
-          icon: 'success',
-          duration: 1500
-        });
-      });
-    },
-    
-    // 获取工作状态文本
-    getWorkStatusText(status) {
-      const statusMap = {
-        working: '工作中',
-        idle: '空闲',
-        error: '故障'
-      };
-      return statusMap[status] || '未知';
-    },
-    
-    // 获取GPS信号文本
-    getGpsSignalText(signal) {
-      if (signal >= 80) return '强';
-      if (signal >= 60) return '中等';
-      if (signal >= 40) return '弱';
-      return '无信号';
-    },
-    
-    // 切换引擎状态
-    toggleEngine() {
-      if (this.deviceInfo.status === 'offline') {
-        uni.showToast({
-          title: '设备离线,无法操作',
-          icon: 'none'
-        });
-        return;
-      }
-      
-      this.isEngineOn = !this.isEngineOn;
-      
-      if (this.isEngineOn) {
-        this.deviceInfo.workStatus = 'idle';
-        uni.showToast({
-          title: '引擎已启动',
-          icon: 'success'
-        });
-      } else {
-        this.deviceInfo.workStatus = 'idle';
-        this.currentSpeed = 0;
-        this.stopAllControls();
-        uni.showToast({
-          title: '引擎已关闭',
-          icon: 'success'
-        });
-      }
-    },
-    
-    // 开始控制
-    startControl(direction) {
-      if (!this.isEngineOn || this.deviceInfo.status === 'offline') {
-        return;
-      }
-      
-      this.activeControl = direction;
-      this.deviceInfo.workStatus = 'working';
-      
-      // 模拟速度变化
-      switch (direction) {
-        case 'forward':
-          this.currentSpeed = 5;
-          break;
-        case 'backward':
-          this.currentSpeed = 2;
-          break;
-        case 'left':
-        case 'right':
-          this.currentSpeed = 3;
-          break;
-      }
-      
-      // 发送控制指令
-      this.sendControlCommand(direction, true);
-    },
-    
-    // 停止控制
-    stopControl() {
-      if (this.activeControl) {
-        this.sendControlCommand(this.activeControl, false);
-      }
-      
-      this.activeControl = '';
-      this.currentSpeed = 0;
-      this.deviceInfo.workStatus = 'idle';
-    },
-    
-    // 停止所有控制
-    stopAllControls() {
-      this.activeControl = '';
-      this.currentSpeed = 0;
-      if (this.isEngineOn) {
-        this.deviceInfo.workStatus = 'idle';
-      }
-    },
-    
-    // 发送控制指令
-    sendControlCommand(direction, isStart) {
-      console.log(`发送控制指令: ${direction}, 开始: ${isStart}`);
-      // 这里可以调用API发送实际的控制指令
-    },
-    
-    // 紧急停止
-    emergencyStop() {
-      if (this.deviceInfo.status === 'offline') {
-        uni.showToast({
-          title: '设备离线,无法操作',
-          icon: 'none'
-        });
-        return;
+          })
+        } finally {
+          uni.hideLoading()
+        }
       }
-      
-      uni.showModal({
-        title: '紧急停止',
-        content: '确定要执行紧急停止吗?设备将立即停止所有操作。',
-        success: (res) => {
-          if (res.confirm) {
-            this.isEngineOn = false;
-            this.stopAllControls();
-            
-            uni.showToast({
-              title: '紧急停止已执行',
-              icon: 'success'
-            });
+    }
+  })
+}
+
+// 删除作业
+const confirmDeleteTask = (task) => {
+  if (!task || !task.id) return
+
+  uni.showModal({
+    title: '确认删除',
+    content: `确定删除作业「${task.name}」吗?删除后不可恢复。`,
+    confirmText: '删除',
+    confirmColor: '#F56C6C',
+    success: async (res) => {
+      if (!res.confirm) return
+
+      try {
+        uni.showLoading({ title: '删除中...' })
+        const resp = await deleteTask(task.id)
+        const { data } = resp || {}
+        if (data && data.code === 200) {
+          uni.showToast({ title: '删除成功', icon: 'success' })
+
+          if (selectedTaskId.value === task.id) {
+            selectedTaskId.value = null
           }
+
+          await loadTaskList()
+        } else {
+          uni.showToast({
+            title: (data && data.msg) ? data.msg : '删除失败',
+            icon: 'none'
+          })
         }
-      });
-    },
-    
-    // 返回充电
-    returnHome() {
-      if (this.deviceInfo.status === 'offline') {
-        uni.showToast({
-          title: '设备离线,无法操作',
-          icon: 'none'
-        });
-        return;
-      }
-      
-      uni.showToast({
-        title: '设备正在返回充电站',
-        icon: 'success'
-      });
-    },
-    
-    // 自动模式
-    autoMode() {
-      if (this.deviceInfo.status === 'offline') {
-        uni.showToast({
-          title: '设备离线,无法操作',
-          icon: 'none'
-        });
-        return;
-      }
-      
-      uni.showToast({
-        title: '已切换到自动模式',
-        icon: 'success'
-      });
-    },
-
-    // 跳转到新增作业页面
-    goToCreateJob() {
-      uni.navigateTo({
-        url: `/pages/device/job-create/index?machineCode=${this.deviceInfo.machineCode}&id=${this.deviceInfo.id}`
-      });
-    },
-
-    // 选择作业任务
-    selectTask(task) {
-      this.selectedTaskId = task.id;
-    },
-
-    // 开始作业
-    startWork() {
-      if (!this.taskList.length) {
-        uni.showToast({
-          title: '暂无可执行的作业任务',
-          icon: 'none'
-        });
-        return;
-      }
-      if (!this.selectedTaskId) {
-        uni.showToast({
-          title: '请先选择一个作业任务',
-          icon: 'none'
-        });
-        return;
+      } catch (e) {
+        console.error('删除作业失败:', e)
+        uni.showToast({ title: '网络异常,删除失败', icon: 'none' })
+      } finally {
+        uni.hideLoading()
       }
-      if (this.deviceInfo.onlineStatus !== 1) {
+    }
+  })
+}
+
+// 处理告警
+const handleAlert = (alert) => {
+  uni.showModal({
+    title: alert.title,
+    content: alert.description + '\n\n是否标记为已处理?',
+    success: (res) => {
+      if (res.confirm) {
+        const index = alerts.value.findIndex(item => item.id === alert.id)
+        if (index !== -1) {
+          alerts.value[index].handled = true
+        }
+        
         uni.showToast({
-          title: '设备未在线,无法开始作业',
-          icon: 'none'
-        });
-        return;
+          title: '已标记为处理',
+          icon: 'success'
+        })
       }
-
-      const task = this.taskList.find(t => t.id === this.selectedTaskId);
-      uni.showModal({
-        title: '开始作业',
-        content: `确定开始执行「${task.name}」吗?`,
-        success: async (res) => {
-          if (res.confirm) {
-            try {
-              uni.showLoading({ title: '启动中...' });
-              const resp = await startTask(this.selectedTaskId);
-              const { data } = resp || {};
-              if (data && data.code === 200) {
-                uni.showToast({
-                  title: '作业已启动',
-                  icon: 'success'
-                });
-                this.isEngineOn = true;
-                this.deviceInfo.workStatus = 'working';
-
-                // 跳转到作业详情页
-                setTimeout(() => {
-                  uni.navigateTo({
-                    url: `/pages/device/job-detail/index?id=${this.selectedTaskId}&deviceId=${this.deviceInfo.machineCode}&deviceName=${this.deviceInfo.machineName}`
-                  });
-                }, 300);
-              } else {
-                uni.showToast({
-                  title: (data && data.msg) ? data.msg : '启动失败',
-                  icon: 'none'
-                });
-              }
-            } catch (e) {
-              console.error('开始作业失败:', e);
-              uni.showToast({
-                title: '网络异常,启动失败',
-                icon: 'none'
-              });
-            } finally {
-              uni.hideLoading();
-            }
-          }
-        }
-      });
-    },
-    
-    // 删除作业
-    confirmDeleteTask(task) {
-      if (!task || !task.id) return
-
-      uni.showModal({
-        title: '确认删除',
-        content: `确定删除作业「${task.name}」吗?删除后不可恢复。`,
-        confirmText: '删除',
-        confirmColor: '#F56C6C',
-        success: async (res) => {
-          if (!res.confirm) return
-
-          try {
-            uni.showLoading({ title: '删除中...' })
-            const resp = await deleteTask(task.id)
-            const { data } = resp || {}
-            if (data && data.code === 200) {
-              uni.showToast({ title: '删除成功', icon: 'success' })
-
-              // 若删除的是当前选中任务,清理选中态
-              if (this.selectedTaskId === task.id) {
-                this.selectedTaskId = null
-              }
-
-              // 重新拉取列表
-              await this.loadTaskList()
-            } else {
-              uni.showToast({
-                title: (data && data.msg) ? data.msg : '删除失败',
-                icon: 'none'
-              })
-            }
-          } catch (e) {
-            console.error('删除作业失败:', e)
-            uni.showToast({ title: '网络异常,删除失败', icon: 'none' })
-          } finally {
-            uni.hideLoading()
-          }
-        }
-      })
-    },
-
-    // 处理告警
-    handleAlert(alert) {
-      uni.showModal({
-        title: alert.title,
-        content: alert.description + '\n\n是否标记为已处理?',
-        success: (res) => {
-          if (res.confirm) {
-            const index = this.alerts.findIndex(item => item.id === alert.id);
-            if (index !== -1) {
-              this.alerts[index].handled = true;
-            }
-            
-            uni.showToast({
-              title: '已标记为处理',
-              icon: 'success'
-            });
-          }
-        }
-      });
     }
-  }
-}
+  })
+}
+
+// 生命周期钩子
+onMounted(() => {
+	// uni-app 生命周期
+	uni.$once('agriculturalMachinesData', (data) => {
+	  if (data) {
+	    deviceInfo.machineCode = data.machineCode
+	    deviceInfo.machineName = data.machineName
+	    deviceInfo.onlineStatus = data.onlineStatus
+	    deviceInfo.updateTime = data.updateTime
+	  }
+	})
+  uni.setNavigationBarTitle({
+    title: '农机设备详情'
+  })
+  
+  loadDeviceData()
+  loadTaskList()
+  loadDeviceAlarmData()
+})
+onLoad((options)=>{
+	deviceInfo.id = options.id;
+})
+
 </script>
 
 <style scoped>

+ 362 - 363
pages/device/device-list/index.vue

@@ -171,364 +171,364 @@
   </view>
 </template>
 
-<script>
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { onLoad} from '@dcloudio/uni-app'
 import { fetchDevicesByType } from "@/api/services/device.js";
 import { machinesDeviceList } from "@/api/services/agriculturalMachines.js";
 import storage from "@/utils/storage.js";
 
-export default {
-  data() {
-    return {
-      deviceType: '', // monitor, sensor, control, irrigation, tractor
-      deviceTypeName: '',
-      deviceList: [],
-      searchKey: '',
-      isSearchFocused: false,
-      currentStatus: -1, // -1代表全部
-      pageNum: 1,
-      pageSize: 10,
-      total: 0,
-      loading: false,
-      isRefreshing: false,
-      loadMoreStatus: 'more', // 加载更多状态: more-加载更多 loading-加载中 noMore-没有更多了
-      deviceTypeMap: {
-        'monitor': { name: '监控设备', icon: '/static/icons/camera.png', class: 'type-monitor' },
-        'sensor': { name: '采集设备', icon: '/static/icons/sensor.png', class: 'type-sensor' },
-        'control': { name: '控制设备', icon: '/static/icons/control.png', class: 'type-control' },
-        'irrigation': { name: '灌溉设备', icon: '/static/icons/water.png', class: 'type-irrigation' },
-        'tractor': { name: '农机设备', icon: '/static/icons/tractor.png', class: 'type-tractor' }
-      },
-      currentFieldId: null,
-	  onlineDevices:0,
-	  offlineDevices:0
-    };
-  },
+// 响应式数据
+const deviceType = ref('') // monitor, sensor, control, irrigation, tractor
+const deviceTypeName = ref('')
+const deviceList = ref([])
+const searchKey = ref('')
+const isSearchFocused = ref(false)
+const currentStatus = ref(-1) // -1代表全部
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const loading = ref(false)
+const isRefreshing = ref(false)
+const loadMoreStatus = ref('more') // 加载更多状态: more-加载更多 loading-加载中 noMore-没有更多了
+const deviceTypeMap = reactive({
+  'monitor': { name: '监控设备', icon: '/static/icons/camera.png', class: 'type-monitor' },
+  'sensor': { name: '采集设备', icon: '/static/icons/sensor.png', class: 'type-sensor' },
+  'control': { name: '控制设备', icon: '/static/icons/control.png', class: 'type-control' },
+  'irrigation': { name: '灌溉设备', icon: '/static/icons/water.png', class: 'type-irrigation' },
+  'tractor': { name: '农机设备', icon: '/static/icons/tractor.png', class: 'type-tractor' }
+})
+const currentFieldId = ref(null)
+const onlineDevices = ref(0)
+const offlineDevices = ref(0)
+
+// 计算属性
+// 设备类型对应的样式类
+const deviceTypeClass = computed(() => {
+  return deviceTypeMap[deviceType.value]?.class || ''
+})
+
+// uni-app 生命周期 - onLoad
+onLoad((options) => {
+  // 获取传递的设备类型
+  console.log("options类型:", options);
+  const { type, typeOnline, typeOffline } = options;
+  // 处理传递过来的统计数量
+  offlineDevices.value = typeOffline
+  onlineDevices.value = typeOnline
+  if (type && deviceTypeMap[type]) {
+    deviceType.value = type;
+    deviceTypeName.value = deviceTypeMap[type].name;
+  }
   
-  computed: {
-    // 设备类型对应的样式类
-    deviceTypeClass() {
-      return this.deviceTypeMap[this.deviceType]?.class || '';
-    }
-  },
+  // 获取当前地块ID
+  initFieldInfo();
   
-  onLoad(options) {
-    // 获取传递的设备类型
-    console.log("options类型:", options);
-    const { type, typeOnline, typeOffline } = options;
-	// 处理传递过来的统计数量
-	this.offlineDevices = typeOffline
-	this.onlineDevices = typeOnline
-    if (type && this.deviceTypeMap[type]) {
-      this.deviceType = type;
-      this.deviceTypeName = this.deviceTypeMap[type].name;
-    }
+  // 加载设备列表
+  deviceType.value === 'tractor' ? loadMachinesList() : loadDeviceList();
+})
+
+// 方法定义
+const loadMachinesList = () => {
+  machinesDeviceList({
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+
+  }).then(res => {
+    console.log("res收到发斯蒂芬斯蒂",res);
     
-    // 获取当前地块ID
-    this.initFieldInfo();
-    
-    // 加载设备列表
-    this.deviceType === 'tractor' ? this.loadMachinesList() : this.loadDeviceList();
-    // this.loadDeviceList();
-  },
+    res.data.code === 200 && res.data.rows && (deviceList.value = res.data.rows);
+    res.data.code !== 200 && handleApiError(res);
+  }).catch(error => {
+    console.error('获取农机设备列表失败', error);
+    uni.showToast({
+      title: '获取农机设备列表失败',
+      icon: 'none'
+    });
+  }).finally(() => {
+    loading.value = false;
+  });
+}
+
+// 初始化地块信息
+const initFieldInfo = () => {
+  const currentPlots = JSON.parse(storage.getPlots() || '{}');
+  if (currentPlots) {
+    currentFieldId.value = currentPlots.id;
+  }
+}
+
+// 获取特定状态的设备数量
+const getStatusCount = (status) => {
+  return deviceList.value.filter(device => device.status === status).length;
+}
+
+// 加载设备列表
+const loadDeviceList = (reset = true) => {
+  if (loading.value) return;
   
-  methods: {
-    loadMachinesList() {
-      machinesDeviceList({
-        pageNum: this.pageNum,
-        pageSize: this.pageSize,
-
-      }).then(res => {
-        console.log("res收到发斯蒂芬斯蒂",res);
+  if (reset) {
+    pageNum.value = 1;
+    deviceList.value = [];
+  }
+  
+  loading.value = true;
+  loadMoreStatus.value = 'loading';
+  
+  // 构建查询参数
+  const params = {
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+    deviceQueryParams: searchKey.value || undefined,
+    deviceType: deviceType.value || undefined,
+    fieldId: currentFieldId.value || undefined
+  };
+  
+  // 如果状态不是全部,添加状态筛选
+  if (currentStatus.value !== -1) {
+    params.status = currentStatus.value;
+  }
+  
+  // 调用API获取设备列表
+  fetchDevicesByType(params)
+    .then(res => {
+      console.log("res", res);
+      if (res.data.code === 200 && res.data.rows) {
+        const { rows, total: totalCount } = res.data;
         
-        res.data.code === 200 && res.data.rows && (this.deviceList = res.data.rows);
-        res.data.code !== 200 && this.handleApiError(res);
-      }).catch(error => {
-        console.error('获取农机设备列表失败', error);
-        uni.showToast({
-          title: '获取农机设备列表失败',
-          icon: 'none'
+        // 更新设备列表
+        if (reset) {
+          deviceList.value = rows;
+        } else {
+          deviceList.value = [...deviceList.value, ...rows];
+        }
+        
+        total.value = totalCount;
+        // 为农技增加模拟数据 后续可删除
+        if(deviceType.value === 'tractor' ){
+          const newDevices = generateMockDevices();
+          console.log("newDevices",newDevices);
+          deviceList.value = [...deviceList.value, ...newDevices];
+          total.value = deviceList.value.length
+        }
+        
+        // 标记有告警的设备
+        deviceList.value.forEach(device => {
+          // 这里可以根据实际情况设置hasAlert属性
+          device.hasAlert = false; // 示例,实际应该根据后台数据判断
         });
-      }).finally(() => {
-        this.loading = false;
-      });
-    },
-    // 初始化地块信息
-    initFieldInfo() {
-      const currentPlots = JSON.parse(storage.getPlots() || '{}');
-      if (currentPlots) {
-        this.currentFieldId = currentPlots.id;
-      }
-    },
-    
-    // 获取特定状态的设备数量
-    getStatusCount(status) {
-      return this.deviceList.filter(device => device.status === status).length;
-    },
-    
-    // 加载设备列表
-    loadDeviceList(reset = true) {
-      if (this.loading) return;
-      
-      if (reset) {
-        this.pageNum = 1;
-        this.deviceList = [];
-      }
-      
-      this.loading = true;
-      this.loadMoreStatus = 'loading';
-      
-      // 构建查询参数
-      const params = {
-        pageNum: this.pageNum,
-        pageSize: this.pageSize,
-        deviceQueryParams: this.searchKey || undefined,
-        deviceType: this.deviceType || undefined,
-        fieldId: this.currentFieldId || undefined
-      };
-      
-      // 如果状态不是全部,添加状态筛选
-      if (this.currentStatus !== -1) {
-        params.status = this.currentStatus;
+        
+        // 更新加载更多状态
+        if (deviceList.value.length >= totalCount) {
+          loadMoreStatus.value = 'noMore';
+        } else {
+          loadMoreStatus.value = 'more';
+        }
+      } else {
+        handleApiError(res);
       }
-      
-      // 调用API获取设备列表
-      fetchDevicesByType(params)
-        .then(res => {
-          console.log("res", res);
-          if (res.data.code === 200 && res.data.rows) {
-            const { rows, total } = res.data;
-            
-            // 更新设备列表
-            if (reset) {
-              this.deviceList = rows;
-            } else {
-              this.deviceList = [...this.deviceList, ...rows];
-            }
-            
-            this.total = total;
-			// 为农技增加模拟数据 后续可删除
-            if(this.deviceType === 'tractor' ){
-            	const newDevices = this.generateMockDevices();
-				console.log("newDevices",newDevices);
-            	this.deviceList = [...this.deviceList, ...newDevices];
-				this.total = this.deviceList.length
-            }
-			
-            // 标记有告警的设备
-            this.deviceList.forEach(device => {
-              // 这里可以根据实际情况设置hasAlert属性
-              device.hasAlert = false; // 示例,实际应该根据后台数据判断
-            });
-            
-            // 更新加载更多状态
-            if (this.deviceList.length >= total) {
-              this.loadMoreStatus = 'noMore';
-            } else {
-              this.loadMoreStatus = 'more';
-            }
-          } else {
-            this.handleApiError(res);
-          }
-        })
-        .catch(error => {
-          console.error('获取设备列表失败', error);
-          uni.showToast({
-            title: '获取设备列表失败',
-            icon: 'none'
-          });
-          this.loadMoreStatus = 'more';
-        })
-        .finally(() => {
-          this.loading = false;
-          this.isRefreshing = false;
-          uni.hideLoading();
-        });
-    },
-    
-    // 处理API错误
-    handleApiError(res) {
-      console.error('API错误', res);
+    })
+    .catch(error => {
+      console.error('获取设备列表失败', error);
       uni.showToast({
-        title: res.msg || '获取数据失败',
+        title: '获取设备列表失败',
         icon: 'none'
       });
-    },
-    
-    // 搜索
-    onSearch() {
-      this.loadDeviceList();
-    },
-    
-    // 清除搜索
-    clearSearch() {
-      this.searchKey = '';
-      this.loadDeviceList();
-    },
-    
-    // 选择状态
-    selectStatus(value) {
-      this.currentStatus = value;
-      this.loadDeviceList();
-    },
-    
-    // 处理下拉刷新
-    handleRefresh() {
-      this.isRefreshing = true;
-      this.loadDeviceList();
-    },
-    
-    // 跳转到设备详情页
-    navigateToDeviceDetail(device) {
-		console.log("device",device);
-      // 根据设备类型跳转到不同的详情页
-      let url = '';
-      // 先发送事件
-      uni.$emit('passDeviceData', {
-        deviceId: device.deviceId,
-        deviceTypeId: device.deviceTypeId,
-        fieldName: device.fieldName,
-        deviceName: device.deviceName
-      });
-      if (device.deviceTypeId === '2') {
-        url = `/pages/device/device-list/detail-camera?id=${device.id}`;
-      } else if (device.deviceTypeId === '1' || device.deviceTypeId === '4') {
-        // 采集设备跳转到采集设备详情页,同时传递设备编码,便于判断设备子类型
-        // url = `/pages/device/device-list/detail-collector?id=${device.deviceId}&deviceTypeId=${device.deviceTypeId}&fieldName=${device.fieldName}&deviceName=${device.deviceName}`;
-		url = `/pages/device/device-list/detail-collector`;
-      
-      } else if (device.type === 'tractor') {
-        // 农机设备跳转到农机设备详情页
-        url = `/pages/device/device-list/detail-machine?id=${device.id}&deviceId=${device.code}`;
-      } else {
-        // 其他类型设备暂时使用通用详情页
-        url = `/pages/device/device-detail/index?id=${device.id}&type=${device.type}`;
+      loadMoreStatus.value = 'more';
+    })
+    .finally(() => {
+      loading.value = false;
+      isRefreshing.value = false;
+      uni.hideLoading();
+    });
+}
+
+// 处理API错误
+const handleApiError = (res) => {
+  console.error('API错误', res);
+  uni.showToast({
+    title: res.msg || '获取数据失败',
+    icon: 'none'
+  });
+}
+
+// 搜索
+const onSearch = () => {
+  loadDeviceList();
+}
+
+// 清除搜索
+const clearSearch = () => {
+  searchKey.value = '';
+  loadDeviceList();
+}
+
+// 选择状态
+const selectStatus = (value) => {
+  currentStatus.value = value;
+  loadDeviceList();
+}
+
+// 处理下拉刷新
+const handleRefresh = () => {
+  isRefreshing.value = true;
+  loadDeviceList();
+}
+
+// 跳转到设备详情页
+const navigateToDeviceDetail = (device) => {
+  console.log("device",device);
+  // 根据设备类型跳转到不同的详情页
+  let url = '';
+  // 先发送事件
+  uni.$emit('passDeviceData', {
+    deviceId: device.deviceId,
+    deviceTypeId: device.deviceTypeId,
+    fieldName: device.fieldName,
+    deviceName: device.deviceName
+  });
+  if (device.deviceTypeId === '2') {
+    url = `/pages/device/device-list/detail-camera?id=${device.id}`;
+  } else if (device.deviceTypeId === '1' || device.deviceTypeId === '4') {
+    // 采集设备跳转到采集设备详情页,同时传递设备编码,便于判断设备子类型
+    // url = `/pages/device/device-list/detail-collector?id=${device.deviceId}&deviceTypeId=${device.deviceTypeId}&fieldName=${device.fieldName}&deviceName=${device.deviceName}`;
+    url = `/pages/device/device-list/detail-collector`;
+  
+  } else if (device.type === 'tractor') {
+    // 农机设备跳转到农机设备详情页
+    url = `/pages/device/device-list/detail-machine?id=${device.id}&deviceId=${device.code}`;
+  } else {
+    // 其他类型设备暂时使用通用详情页
+    url = `/pages/device/device-detail/index?id=${device.id}&type=${device.type}`;
+  }
+  
+  // 先跳转
+    uni.navigateTo({
+      url: url,
+      success: () => {
+        // 跳转成功后再发送事件,延迟一点确保页面onLoad注册完成
+        setTimeout(() => {
+          uni.$emit('passDeviceData', {
+            deviceId: device.deviceId,
+            deviceTypeId: device.deviceTypeId,
+            fieldName: device.fieldName,
+            deviceName: device.deviceName,
+            status:device.status
+          });
+        }, 100); // 100ms 通常足够,必要时可加到 200
+      },
+      fail: (e)=>{
+        console.log(e);
       }
-      
-      // 先跳转
-        uni.navigateTo({
-          url: url,
-          success: () => {
-            // 跳转成功后再发送事件,延迟一点确保页面onLoad注册完成
-            setTimeout(() => {
-              uni.$emit('passDeviceData', {
-                deviceId: device.deviceId,
-                deviceTypeId: device.deviceTypeId,
-                fieldName: device.fieldName,
-                deviceName: device.deviceName,
-				        status:device.status
-              });
-            }, 100); // 100ms 通常足够,必要时可加到 200
-          },
-		  fail: (e)=>{
-			  console.log(e);
-		  }
-        });
-    },
+    });
+}
+
+// 获取设备类型图标
+const getDeviceTypeIcon = (typeId) => {
+  // 根据后端设备类型ID获取对应前端类型的图标
+  const typeMapping = {
+    '1': 'sensor', // 传感器
+    '2': 'monitor', // 摄像头
+    '3': 'control', // 控制器
+    '4': 'irrigation', // 气象设备/灌溉设备
+    '5': 'tractor'  // 农机设备
+  };
+  
+  const frontendType = typeMapping[typeId] || deviceType.value;
+  return deviceTypeMap[frontendType]?.icon || '/static/icons/device.png';
+}
+
+// 获取设备类型名称
+const getDeviceTypeName = (typeId) => {
+  const typeNames = {
+    '1': '采集设备',
+    '2': '监控设备',
+    '3': '控制设备',
+    '4': '灌溉设备',
+    '5': '农机设备'
+  };
+  
+  return typeNames[typeId] || '未知类型';
+}
+
+// 获取状态文本
+const getStatusText = (status) => {
+  const statusMap = {
+    0: '离线',
+    1: '在线',
+    2: '故障',
+    3: '维护中'
+  };
+  
+  return statusMap[status] || '未知状态';
+}
+
+// 格式化日期
+const formatDate = (dateStr) => {
+  if (!dateStr) return '未知';
+  
+  // 解析为 Date
+  const parsedStr = dateStr.replace(' ', 'T'); 
+  const date = new Date(parsedStr);
+  if (isNaN(date)) return '无效时间';
+  
+  const now = new Date();
+
+  // 计算差时长(分钟)
+  const diff = Math.floor((now - date) / 1000 / 60);
+  
+  if (diff < 1) return '刚刚更新';
+  if (diff < 5) return '1分钟前更新';
+  if (diff < 10) return '5分钟前更新';
+  if (diff < 60) return `${diff}分钟前更新`;
+  
+  if (diff < 120) return '1小时前更新';
+  if (diff < 24 * 60) return `${Math.floor(diff / 60)}小时前更新`;
+  if (diff < 7 * 24 * 60) return `${Math.floor(diff / (60 * 24))}天前更新`;
+  
+  return parsedStr.split('T')[0] + ' 更新';
+}
+
+// 生成模拟设备数据
+const generateMockDevices = () => {
+  const devices = [];
+  const locations = ['东区A1地块', '西区B2地块', '南区C3地块', '北区D4地块'];
+  const updateTimes = ['刚刚更新', '1分钟前更新', '5分钟前更新', '10分钟前更新', '1小时前更新'];
+  
+  // 根据当前页码和限制数量生成对应数量的模拟数据
+  const startIndex = (pageNum.value - 1) * pageSize.value;
+  for (let i = 0; i < pageSize.value; i++) {
+    const index = startIndex + i;
     
-    // 获取设备类型图标
-    getDeviceTypeIcon(typeId) {
-      // 根据后端设备类型ID获取对应前端类型的图标
-      const typeMapping = {
-        '1': 'sensor', // 传感器
-        '2': 'monitor', // 摄像头
-        '3': 'control', // 控制器
-        '4': 'irrigation', // 气象设备/灌溉设备
-        '5': 'tractor'  // 农机设备
-      };
-      
-      const frontendType = typeMapping[typeId] || this.deviceType;
-      return this.deviceTypeMap[frontendType]?.icon || '/static/icons/device.png';
-    },
+    // 如果已经生成了30条数据,则停止
+    if (index >= 30) break;
     
-    // 获取设备类型名称
-    getDeviceTypeName(typeId) {
-      const typeNames = {
-        '1': '采集设备',
-        '2': '监控设备',
-        '3': '控制设备',
-        '4': '灌溉设备',
-        '5': '农机设备'
-      };
-      
-      return typeNames[typeId] || '未知类型';
-    },
+    // 对于采集设备类型,生成随机的气象或土壤设备
+    let devType = deviceType.value;
+    let deviceCode = `DEV${String(index + 1001).padStart(4, '0')}`;
     
-    // 获取状态文本
-    getStatusText(status) {
-      const statusMap = {
-        0: '离线',
-        1: '在线',
-        2: '故障',
-        3: '维护中'
-      };
-      
-      return statusMap[status] || '未知状态';
-    },
+    // 如果是采集设备,随机生成气象站或土壤墒情设备
+    if (deviceType.value === 'sensor') {
+      // 随机分配采集设备子类型:气象站或土壤墒情
+      const sensorSubType = Math.random() > 0.5 ? 'weather' : 'soil';
+      deviceCode = sensorSubType === 'weather' ? `W${deviceCode}` : `S${deviceCode}`;
+    }
     
-    // 格式化日期
-	formatDate(dateStr) {
-	  if (!dateStr) return '未知';
-	  
-	  // 解析为 Date
-	  const parsedStr = dateStr.replace(' ', 'T'); 
-	  const date = new Date(parsedStr);
-	  if (isNaN(date)) return '无效时间';
-	  
-	  const now = new Date();
-	
-	  // 计算差时长(分钟)
-	  const diff = Math.floor((now - date) / 1000 / 60);
-	  
-	  if (diff < 1) return '刚刚更新';
-	  if (diff < 5) return '1分钟前更新';
-	  if (diff < 10) return '5分钟前更新';
-	  if (diff < 60) return `${diff}分钟前更新`;
-	  
-	  if (diff < 120) return '1小时前更新';
-	  if (diff < 24 * 60) return `${Math.floor(diff / 60)}小时前更新`;
-	  if (diff < 7 * 24 * 60) return `${Math.floor(diff / (60 * 24))}天前更新`;
-	  
-	  return parsedStr.split('T')[0] + ' 更新';
-	},
-	// 生成模拟设备数据
-	generateMockDevices() {
-	  const devices = [];
-	  const locations = ['东区A1地块', '西区B2地块', '南区C3地块', '北区D4地块'];
-	  const updateTimes = ['刚刚更新', '1分钟前更新', '5分钟前更新', '10分钟前更新', '1小时前更新'];
-	  
-	  // 根据当前页码和限制数量生成对应数量的模拟数据
-	  const startIndex = (this.pageNum - 1) * this.pageSize;
-	  for (let i = 0; i < this.pageSize; i++) {
-	    const index = startIndex + i;
-	    
-	    // 如果已经生成了30条数据,则停止
-	    if (index >= 30) break;
-	    
-	    // 对于采集设备类型,生成随机的气象或土壤设备
-	    let deviceType = this.deviceType;
-	    let deviceCode = `DEV${String(index + 1001).padStart(4, '0')}`;
-	    
-	    // 如果是采集设备,随机生成气象站或土壤墒情设备
-	    if (this.deviceType === 'sensor') {
-	      // 随机分配采集设备子类型:气象站或土壤墒情
-	      const sensorSubType = Math.random() > 0.5 ? 'weather' : 'soil';
-	      deviceCode = sensorSubType === 'weather' ? `W${deviceCode}` : `S${deviceCode}`;
-	    }
-	    
-	    devices.push({
-	      id: `device-${index + 1}`,
-	      deviceName: `${this.getDeviceTypeName(this.deviceType)}-${index + 1}`,
-	      deviceId: deviceCode,
-	      type: deviceType,
-	      status: Math.random() > 0.3 ? 'online' : 'offline', // 70% 概率在线
-	      fieldName: locations[Math.floor(Math.random() * locations.length)],
-	      updateTime: updateTimes[Math.floor(Math.random() * updateTimes.length)],
-	      alarmCount: Math.random() > 0.7 ? Math.floor(Math.random() * 3) + 1 : 0 // 30% 概率有告警
-	    });
-	  }
-	  
-	  return devices;
-	},
+    devices.push({
+      id: `device-${index + 1}`,
+      deviceName: `${getDeviceTypeName(deviceType.value)}-${index + 1}`,
+      deviceId: deviceCode,
+      type: devType,
+      status: Math.random() > 0.3 ? 'online' : 'offline', // 70% 概率在线
+      fieldName: locations[Math.floor(Math.random() * locations.length)],
+      updateTime: updateTimes[Math.floor(Math.random() * updateTimes.length)],
+      alarmCount: Math.random() > 0.7 ? Math.floor(Math.random() * 3) + 1 : 0 // 30% 概率有告警
+    });
+  }
+  
+  return devices;
+}
+
 /*    formatDate(dateStr) {
       if (!dateStr) return '未知';
       
@@ -550,33 +550,32 @@ export default {
       // 超过一周
       return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
     }, */
-    
-    // 页面上拉触底事件
-    onReachBottom() {
-      if (this.loadMoreStatus === 'more') {
-        this.pageNum++;
-        this.loadDeviceList(false);
-      }
-    }
-  },
-  
-  // 下拉刷新
-  onPullDownRefresh() {
-    this.loadDeviceList();
-    setTimeout(() => {
-      uni.stopPullDownRefresh();
-    }, 1000);
-  },
-  
-  // 页面显示
-  onShow() {
-    uni.setNavigationBarTitle({
-      title: this.deviceTypeName || '设备列表'
-    });
+
+// 页面上拉触底事件
+const onReachBottom = () => {
+  if (loadMoreStatus.value === 'more') {
+    pageNum.value++;
+    loadDeviceList(false);
   }
-};
+}
+
+// uni-app 生命周期 - 下拉刷新
+const onPullDownRefresh = () => {
+  loadDeviceList();
+  setTimeout(() => {
+    uni.stopPullDownRefresh();
+  }, 1000);
+}
+
+// uni-app 生命周期 - 页面显示
+const onShow = () => {
+  uni.setNavigationBarTitle({
+    title: deviceTypeName.value || '设备列表'
+  });
+}
 </script>
 
+
 <style scoped>
 /* 图标字体 */
 @font-face {

+ 119 - 123
pages/device/index.vue

@@ -83,139 +83,135 @@
   </view>
 </template>
 
-<script>
+<script setup>
 // 导入API服务
-import { forEach } from "../../utils/lib/request/utils";
-import { fetchDeviceOverview } from "@/api/services/device.js";
-import storage from "@/utils/storage.js";
-
-export default {
-  data() {
-    return {
-      deviceList: [],
-      currentPlot: "加载中...",
-      totalDevices: 0,
-      onlineDevices: 0,
-      offlineDevices: 0,
-      alertDevices: 0,
-      loading: false,
-      currentFieldId: null, // 当前选中的地块ID
-	  typeOnline: 0,
-	  typeOffline: 0
-    }
-  },
+import { ref } from 'vue'
+import { onShow ,onPullDownRefresh} from '@dcloudio/uni-app'
+import { fetchDeviceOverview } from "@/api/services/device.js"
+import storage from "@/utils/storage.js"
+
+// 响应式数据
+const deviceList = ref([])
+const currentPlot = ref("加载中...")
+const totalDevices = ref(0)
+const onlineDevices = ref(0)
+const offlineDevices = ref(0)
+const alertDevices = ref(0)
+const loading = ref(false)
+const currentFieldId = ref(null) // 当前选中的地块ID
+const typeOnline = ref(0)
+const typeOffline = ref(0)
+
+// 初始化地块信息
+const initFieldInfo = () => {
+  const fieldInfo = storage.getPlots()
+  console.log("ggg ")
+  if (fieldInfo) {
+    const plotData = JSON.parse(fieldInfo)
+    currentFieldId.value = plotData.id
+    currentPlot.value = plotData.name || "未选择地块"
+  } else {
+    currentPlot.value = "未选择地块"
+  }
+}
+
+// 获取设备数据
+const fetchDeviceData = () => {
+  if (loading.value) return
+  loading.value = true
+  
+  uni.showLoading({
+    title: '加载中...'
+  })
   
-  methods: {
-    // 初始化地块信息
-    initFieldInfo() {
-      const fieldInfo = storage.getPlots();
-	  console.log("ggg ");
-      if (fieldInfo) {
-		  const plotData = JSON.parse(fieldInfo);
-        this.currentFieldId = plotData.id;
-        this.currentPlot = plotData.name || "未选择地块";
+  // 调用API获取设备概览数据
+  fetchDeviceOverview(currentFieldId.value)
+    .then(res => {
+      if (res.data.code === 200 && res.data.data) {
+        const data = res.data.data
+        console.log("res.data.data",res.data.data);
+        
+        // 更新设备总览数据
+        totalDevices.value = data.totalDevices || 0
+        onlineDevices.value = data.onlineDevices || 0
+        offlineDevices.value = data.offlineDevices || 0
+        alertDevices.value = data.alertDevices || 0
+        
+        // 更新设备类型列表
+        if (data.deviceList && data.deviceList.length > 0) {
+          deviceList.value = data.deviceList
+        }
+        
+        console.log('设备概览数据加载成功')
       } else {
-        this.currentPlot = "未选择地块";
+        handleApiError(res)
       }
-    },
-    
-    // 获取设备数据
-    fetchDeviceData() {
-      if (this.loading) return;
-      this.loading = true;
-      
-      uni.showLoading({
-        title: '加载中...'
-      });
-      
-      // 调用API获取设备概览数据
-      fetchDeviceOverview(this.currentFieldId)
-        .then(res => {
-          if (res.data.code === 200 && res.data.data) {
-            const data = res.data.data;
-            // 更新设备总览数据
-            this.totalDevices = data.totalDevices || 0;
-            this.onlineDevices = data.onlineDevices || 0;
-            this.offlineDevices = data.offlineDevices || 0;
-            this.alertDevices = data.alertDevices || 0;
-            
-            // 更新设备类型列表
-            if (data.deviceList && data.deviceList.length > 0) {
-              this.deviceList = data.deviceList;
-            }
-            
-            console.log('设备概览数据加载成功');
-          } else {
-            this.handleApiError(res);
-          }
-        })
-        .catch(error => {
-          console.error('获取设备概览数据失败', error);
-          uni.showToast({
-            title: '获取设备数据失败',
-            icon: 'none'
-          });
-        })
-        .finally(() => {
-          this.loading = false;
-          uni.hideLoading();
-        });
-    },
-    
-    // 处理API错误
-    handleApiError(res) {
-      console.error('API错误', res);
+    })
+    .catch(error => {
+      console.error('获取设备概览数据失败', error)
       uni.showToast({
-        title: res.data.msg || '获取数据失败',
+        title: '获取设备数据失败',
         icon: 'none'
-      });
-    },
-    
-    // 跳转到对应设备列表页面
-    navigateToDeviceList(type) {
-    console.log("type",type);
-    
-		// 传递指定设备类型的在线、离线数量
-		this.deviceList.forEach((item, index) => {
-			if(item.type === type){
-				this.typeOnline = item.online
-				this.typeOffline =  item.offline
-			}
-		});
-    if(type === 'tractor'){
-      uni.navigateTo({
-        url: `/pages/device/device-list/agricultural/index?type=${type}&typeOnline=${this.typeOnline}&typeOffline=${this.typeOffline}`
-      });
-    }else{
-      uni.navigateTo({
-        url: `/pages/device/device-list/index?type=${type}&typeOnline=${this.typeOnline}&typeOffline=${this.typeOffline}`
-      });
-    }
-    },
-    
-    // 切换地块
-    changePlot() {
-      uni.navigateTo({
-        url: '/pages/field-selector/index?callback=deviceCenter'
-      });
-    },
-
-  },
+      })
+    })
+    .finally(() => {
+      loading.value = false
+      uni.hideLoading()
+    })
+}
+
+// 处理API错误
+const handleApiError = (res) => {
+  console.error('API错误', res)
+  uni.showToast({
+    title: res.data.msg || '获取数据失败',
+    icon: 'none'
+  })
+}
+
+// 跳转到对应设备列表页面
+const navigateToDeviceList = (type) => {
+  console.log("type", type)
   
-  // 页面导航配置
-  onShow() {
-	this.initFieldInfo();
-	this.fetchDeviceData();
-  },
+  // 传递指定设备类型的在线、离线数量
+  deviceList.value.forEach((item, index) => {
+    if (item.type === type) {
+      typeOnline.value = item.online
+      typeOffline.value = item.offline
+    }
+  })
   
-  // 下拉刷新
-  onPullDownRefresh() {
-    this.fetchDeviceData();
-    setTimeout(() => {
-      uni.stopPullDownRefresh();
-    }, 1000);
+  if (type === 'tractor') {
+    uni.navigateTo({
+      url: `/pages/device/device-list/agricultural/index?type=${type}&typeOnline=${typeOnline.value}&typeOffline=${typeOffline.value}`
+    })
+  } else {
+    uni.navigateTo({
+      url: `/pages/device/device-list/index?type=${type}&typeOnline=${typeOnline.value}&typeOffline=${typeOffline.value}`
+    })
   }
 }
+
+// 切换地块
+const changePlot = () => {
+  uni.navigateTo({
+    url: '/pages/field-selector/index?callback=deviceCenter'
+  })
+}
+
+// 页面导航配置 - uni-app 生命周期保持不变
+onShow(() => {
+  initFieldInfo()
+  fetchDeviceData()
+})
+
+// 下拉刷新 - uni-app 生命周期保持不变
+onPullDownRefresh(() => {
+  fetchDeviceData()
+  setTimeout(() => {
+    uni.stopPullDownRefresh()
+  }, 1000)
+})
 </script>
 
 <style scoped>

+ 12 - 1
pages/device/job-create/index.vue

@@ -441,6 +441,7 @@
 <script>
 import { createJob, getRealtimeData } from '@/api/services/job.js'
 import coordinateUtils from '@/utils/coordinateUtils.js'
+import { onLoad, onReady, onUnload} from '@dcloudio/uni-app'
 
     // 地图实例将保存在组件 data 的 `map` 字段中(见 data() 中的 map: null)
     // 我们在组件方法里设置一个绑定到全局回调的适配器,以便 AMap 的 script callback 能够调用组件内的初始化方法。
@@ -523,6 +524,8 @@ export default {
       loopPolygon: null,
       loopReplaceIndex: null,
       // TODO: 将下面的占位 KEY 替换为真实的高德地图 Key(仅 H5 生效)
+      // 原因: 当前使用的是示例 Key,生产环境需要使用真实的高德地图 API Key
+      // 推荐: 从环境变量或配置文件中读取真实的高德地图 Key
       amapKey: '9f2cac7ea18905dd3830cf7360a43a35',
       jscode: '41af52e416d1fd1b15020dac066cec86',
       jobState: {
@@ -614,6 +617,7 @@ export default {
 
   methods: {
     loadScript() { // 挂载动态js
+				// #ifdef H5
 				// 绑定全局回调,AMap 脚本会调用 window.mapInit
 				window.mapInit = () => {
 					this._createMapWhenReady()
@@ -623,16 +627,23 @@ export default {
 				script.src = `https://webapi.amap.com/maps?v=2.0&key=${this.amapKey}&callback=mapInit`;
 				document.body.appendChild(script);
         this.amapLoaded = true
+				// #endif
+				// #ifndef H5
+				// 非 H5 平台使用 uni-app 地图组件
+				console.warn('当前平台不支持动态加载高德地图脚本,请使用 uni-app 地图组件')
+				// #endif
 			},
 
     _createMapWhenReady() {
       const createWhenReady = () => {
+				// #ifdef H5
         const container = document.getElementById('mapContainer')
         if (!container) {
           // 容器尚未渲染,延迟重试
           setTimeout(createWhenReady, 200)
           return
         }
+				// #endif
 
         // 创建基础地图实例并保存在组件 data.map 中
         const defaultCenter = [113.382, 22.5211]
@@ -1195,7 +1206,7 @@ export default {
         (this.jobState.startPointIndex + 1) % total
     },
 
-    // 手动定位
+        // 手动定位
     async manualLocation() {
       if (this.locating) return
       this.locating = true

+ 2305 - 0
pages/device/job-create/index.vue.backup

@@ -0,0 +1,2305 @@
+<template>
+  <view class="page">
+    <!-- 步骤头部 -->
+    <view class="header">
+      <view class="title-row">
+        <text class="title">新增作业</text>
+        <text class="device-id">设备:{{ jobState.machineCode || '未知设备' }}</text>
+      </view>
+      <view class="step-row">
+        <view
+          v-for="step in steps"
+          :key="step.id"
+          class="step-item"
+        >
+          <view
+            class="step-index"
+            :class="{
+              active: currentStep === step.id,
+              done: currentStep > step.id
+            }"
+          >
+            <text v-if="currentStep > step.id">✓</text>
+            <text v-else>{{ step.id }}</text>
+          </view>
+          <view class="step-texts">
+            <text class="step-title">{{ step.title }}</text>
+            <text class="step-sub">{{ step.sub }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 主体内容 -->
+    <view class="content">
+      <!-- Step 1: 选择作业区域类型 & 路线类型 -->
+      <view v-if="currentStep === 1" class="step-block">
+        <view class="card select-card">
+          <view class="select-title">选择作业区域类型</view>
+          <view class="select-sub">
+            不同区域形状将影响后续路线生成方式,请根据实际地块选择。
+          </view>
+          <view class="select-tabs">
+            <view
+              v-for="item in areaTypes"
+              :key="item.value"
+              class="select-tab"
+              :class="{ active: selectedAreaType === item.value }"
+              @click="selectAreaType(item.value)"
+            >
+              <text class="select-tab-label">{{ item.label }}</text>
+              <text class="select-tab-sub" v-if="item.desc">
+                {{ item.desc }}
+              </text>
+            </view>
+          </view>
+        </view>
+
+        <view class="card select-card">
+          <view class="select-title">选择路线类型</view>
+          <view class="select-sub">
+            依据区域形状推荐的路线类型,后端将按所选类型生成具体作业路线。
+          </view>
+          <view class="select-tabs">
+            <view
+              v-for="route in availableRouteTypes"
+              :key="route.value"
+              class="select-tab small"
+              :class="{ active: selectedRouteType === route.value }"
+              @click="selectRouteType(route.value)"
+            >
+              <text class="select-tab-label">{{ route.label }}</text>
+              <text class="select-tab-sub" v-if="route.desc">
+                {{ route.desc }}
+              </text>
+            </view>
+          </view>
+        </view>
+
+        <view class="card tips-card">
+          <view class="tips-title">说明</view>
+          <view class="tips-content">
+            <text>
+              - 本步骤仅选择区域类型与路线类型,下一步将进入地图打点新增作业区域。
+            </text>
+            <text>
+              - 当前选择会随作业一起提交至后端,用于指导路线生成策略。
+            </text>
+          </view>
+        </view>
+      </view>
+
+      <!-- Step 2: 地图打点 -->
+      <view v-else-if="currentStep === 2" class="step-block">
+        <!-- 地图占位 -->
+        <view class="map-card">
+          <view class="map-header">
+            <text class="map-title">地图预览</text>
+            <view class="map-header-actions">
+              <button class="btn-location" @click="manualLocation" :loading="locating">
+                <text class="btn-location-text">{{ locating ? '定位中...' : '📍 定位' }}</text>
+              </button>
+            </view>
+            <text class="map-sub">通过遥控器移动设备,在地图上逐点记录</text>
+          </view>
+          <view class="map-body">
+            <!-- 始终渲染地图容器以避免渲染时序问题;未加载脚本时容器为空白 -->
+            <view id="mapContainer"></view>
+            <text v-if="!amapLoaded" class="map-placeholder">
+              地图占位(H5 平台会加载高德地图 SDK){{
+                '\n'
+              }}当前模式:{{ modeLabel }}
+            </text>
+          </view>
+          <view class="map-footer">
+            <text class="map-hint">
+              提示:请使用遥控器移动设备到拐点位置,再点击“新增点”记录坐标。
+            </text>
+          </view>
+        </view>
+
+        <!-- 模式切换 -->
+        <view class="mode-card">
+          <view class="mode-title-row">
+            <text class="mode-title">点位类型</text>
+            <text class="mode-sub">在不同模式下分别记录作业区域、障碍物和返航点</text>
+          </view>
+          <view class="mode-tabs">
+            <view
+              v-for="item in modes"
+              :key="item.value"
+              class="mode-tab"
+              :class="{ active: mode === item.value }"
+              @click="switchMode(item.value)"
+            >
+              <text class="mode-tab-label">{{ item.label }}</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 控制面板 -->
+        <view class="panel-card">
+          <view class="panel-row">
+            <button class="btn primary" @click="addPoint">新增点</button>
+            <button class="btn ghost" @click="undoPoint" :disabled="!canUndo">
+              撤销
+            </button>
+            <button
+              class="btn ghost"
+              v-if="mode === 'obstacle'"
+              @click="finishCurrentObstacle"
+              :disabled="!currentObstacle.length"
+            >
+              完成当前障碍物
+            </button>
+          </view>
+          <view class="panel-desc">
+            <text v-if="mode === 'area'">
+              作业区域至少需要 3 个点,建议为近似矩形。
+            </text>
+            <text v-else-if="mode === 'obstacle'">
+              每个障碍物可记录多个点,点击“完成当前障碍物”结束本组记录。
+            </text>
+            <text v-else>
+              仅允许 1 个返航点,重复新增会覆盖已有点。
+            </text>
+          </view>
+        </view>
+
+        <!-- 已记录点列表 -->
+        <view class="list-card">
+          <view class="list-title-row">
+            <text class="list-title">已记录点位</text>
+          </view>
+
+          <scroll-view scroll-y class="list-scroll">
+            <!-- 作业区域点 -->
+            <view class="list-group">
+              <view class="list-group-header">
+                <text class="list-group-title">作业区域点({{ jobState.areaPoints.length }})</text>
+              </view>
+              <view
+                v-if="jobState.areaPoints.length"
+                class="point-list"
+              >
+                <view
+                  v-for="(p, index) in jobState.areaPoints"
+                  :key="'area-' + index"
+                  class="point-item"
+                >
+                  <text class="point-label">P{{ index + 1 }}</text>
+                  <text class="point-coord">
+                    {{ formatPoint(p) }}
+                  </text>
+                  <text class="point-time">{{ formatPointTime(p.timestamp) }}</text>
+                </view>
+              </view>
+              <view v-else class="empty-row">
+                <text>暂未记录作业区域点</text>
+              </view>
+            </view>
+
+            <!-- 障碍物点 -->
+            <view class="list-group">
+              <view class="list-group-header">
+                <text class="list-group-title">
+                  障碍物({{ jobState.obstacles.length + (currentObstacle.length ? 1 : 0) }} 组)
+                </text>
+              </view>
+              <view
+                v-if="jobState.obstacles.length || currentObstacle.length"
+                class="obstacle-list"
+              >
+                <!-- 已完成的障碍物组 -->
+                <view
+                  v-for="(obs, oIdx) in jobState.obstacles"
+                  :key="'obs-' + oIdx"
+                  class="obstacle-item"
+                >
+                  <text class="obstacle-title">障碍物 {{ oIdx + 1 }}({{ obs.length }} 点)</text>
+                  <view
+                    v-for="(p, pIdx) in obs"
+                    :key="'obs-' + oIdx + '-' + pIdx"
+                    class="point-item small"
+                  >
+                    <text class="point-label">O{{ oIdx + 1 }}-{{ pIdx + 1 }}</text>
+                    <text class="point-coord">
+                      {{ formatPoint(p) }}
+                    </text>
+                  </view>
+                </view>
+
+                <!-- 正在录入中的障碍物(未点击“完成当前障碍物”之前也要实时回显) -->
+                <view
+                  v-if="currentObstacle.length"
+                  class="obstacle-item"
+                >
+                  <text class="obstacle-title">障碍物 {{ jobState.obstacles.length + 1 }}(录入中,{{ currentObstacle.length }} 点)</text>
+                  <view
+                    v-for="(p, pIdx) in currentObstacle"
+                    :key="'obs-current-' + pIdx"
+                    class="point-item small"
+                  >
+                    <text class="point-label">O{{ jobState.obstacles.length + 1 }}-{{ pIdx + 1 }}</text>
+                    <text class="point-coord">
+                      {{ formatPoint(p) }}
+                    </text>
+                  </view>
+                </view>
+              </view>
+              <view v-else class="empty-row">
+                <text>暂未记录障碍物点</text>
+              </view>
+            </view>
+
+            <!-- 返航点 -->
+            <view class="list-group">
+              <view class="list-group-header">
+                <text class="list-group-title">返航点</text>
+              </view>
+              <view v-if="jobState.returnPoint" class="point-item">
+                <text class="point-label">R</text>
+                <text class="point-coord">
+                  {{ formatPoint(jobState.returnPoint) }}
+                </text>
+                <text class="point-time">
+                  {{ formatPointTime(jobState.returnPoint.timestamp) }}
+                </text>
+              </view>
+              <view v-else class="empty-row">
+                <text>暂未设置返航点</text>
+              </view>
+            </view>
+          </scroll-view>
+        </view>
+      </view>
+
+      <!-- Step 3: 起点选择 -->
+      <view v-else-if="currentStep === 3" class="step-block">
+        <view class="map-card small">
+          <view class="map-header">
+            <text class="map-title">选择起点</text>
+            <text class="map-sub">
+              从已记录的作业区域点中选择作业起点,可通过左右切换预览。
+            </text>
+          </view>
+          <view class="map-body">
+            <text class="map-placeholder">
+              这里显示作业区域示意图(占位){{
+                '\n'
+              }}当前起点:P{{ currentStartDisplay }}
+            </text>
+          </view>
+        </view>
+
+        <view class="panel-card">
+          <view class="panel-row center">
+            <button class="btn ghost" @click="prevStart" :disabled="!canChangeStart">
+              上一个
+            </button>
+            <view class="start-index">
+              <text class="start-index-text">
+                起点:P{{ currentStartDisplay }}
+              </text>
+            </view>
+            <button class="btn ghost" @click="nextStart" :disabled="!canChangeStart">
+              下一个
+            </button>
+          </view>
+          <view class="panel-desc">
+            <text>
+              提示:起点将决定设备的初始行进方向与作业顺序,后端会基于该起点生成具体路线。
+            </text>
+          </view>
+        </view>
+
+        <view class="list-card">
+          <view class="list-title-row">
+            <text class="list-title">作业区域点列表</text>
+          </view>
+          <scroll-view scroll-y class="list-scroll">
+            <view
+              v-for="(p, index) in jobState.areaPoints"
+              :key="'start-' + index"
+              class="point-item"
+              :class="{ active: index === jobState.startPointIndex }"
+              @click="setStartIndex(index)"
+            >
+              <text class="point-label">P{{ index + 1 }}</text>
+              <text class="point-coord">{{ formatPoint(p) }}</text>
+            </view>
+          </scroll-view>
+        </view>
+      </view>
+
+      <!-- Step 4: 作业信息确认 -->
+      <view v-else-if="currentStep === 4" class="step-block">
+        <view class="card confirm-card">
+          <view class="confirm-title">
+            <text>作业基本信息</text>
+          </view>
+
+          <view class="form-item required">
+            <text class="label">作业名称</text>
+            <input
+              class="input"
+              v-model="jobState.jobName"
+              placeholder="请输入作业名称,如“Test device - 北区作业”"
+            />
+          </view>
+
+          <view class="form-item required">
+            <text class="label">地块ID</text>
+            <input
+              class="input"
+              v-model="jobState.fieldId"
+              placeholder="请输入地块ID"
+              type="text"
+            />
+          </view>
+
+          <view class="form-item required">
+            <text class="label">路径宽度(厘米)</text>
+            <input
+              class="input"
+              v-model="jobState.pathWidth"
+              placeholder="请输入路径宽度,单位:厘米"
+              type="number"
+            />
+          </view>
+
+          <view class="summary-row">
+            <text class="summary-label">作业区域类型</text>
+            <text class="summary-value">{{ areaTypeLabel }}</text>
+          </view>
+          <view class="summary-row">
+            <text class="summary-label">路线类型</text>
+            <text class="summary-value">{{ routeTypeLabel }}</text>
+          </view>
+          <view class="summary-row">
+            <text class="summary-label">作业区域点</text>
+            <text class="summary-value">{{ jobState.areaPoints.length }} 个</text>
+          </view>
+          <view class="summary-row">
+            <text class="summary-label">障碍物</text>
+            <text class="summary-value">
+              {{ jobState.obstacles.length }} 组
+            </text>
+          </view>
+          <view class="summary-row">
+            <text class="summary-label">返航点</text>
+            <text class="summary-value">
+              {{ jobState.returnPoint ? '已设置' : '未设置' }}
+            </text>
+          </view>
+          <view class="summary-row">
+            <text class="summary-label">起点索引</text>
+            <text class="summary-value">
+              P{{ currentStartDisplay }}
+            </text>
+          </view>
+        </view>
+
+        <view class="card tips-card">
+          <view class="tips-title">说明</view>
+          <view class="tips-content">
+            <text>
+              - 前端仅负责记录点位与基本配置,并在本页面完成数据完整性校验。
+            </text>
+            <text>
+              - 路线生成、几何合法性校验以及调度逻辑由后端 `/api/job/create` 负责处理。
+            </text>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 步骤导航 -->
+    <view class="footer">
+      <button class="btn ghost" @click="prevStep" :disabled="currentStep === 1">
+        上一步
+      </button>
+      <button
+        class="btn primary"
+        v-if="currentStep < 4"
+        @click="nextStep"
+      >
+        下一步
+      </button>
+      <button
+        class="btn primary"
+        v-else
+        :loading="submitting"
+        @click="submitJob"
+      >
+        完成并提交
+      </button>
+    </view>
+  </view>
+</template>
+
+<script>
+import { createJob, getRealtimeData } from '@/api/services/job.js'
+import coordinateUtils from '@/utils/coordinateUtils.js'
+
+    // 地图实例将保存在组件 data 的 `map` 字段中(见 data() 中的 map: null)
+    // 我们在组件方法里设置一个绑定到全局回调的适配器,以便 AMap 的 script callback 能够调用组件内的初始化方法。
+// 简单模拟一个“当前设备坐标”,后续可替换为真实位置上报
+function mockCurrentPoint(baseIndex = 0) {
+  const now = Date.now()
+  const lng = 120.0 + (baseIndex % 10) * 0.0001
+  const lat = 30.0 + (baseIndex % 10) * 0.0001
+  return {
+    lng,
+    lat,
+    timestamp: now
+  }
+}
+
+export default {
+  data() {
+    return {
+      currentStep: 1,
+      steps: [
+        { id: 1, title: '区域与路线', sub: '选择作业区域类型与路线类型' },
+        { id: 2, title: '打点建模', sub: '作业区域 / 障碍物 / 返航点' },
+        { id: 3, title: '选择起点', sub: '确定作业起点位置' },
+        { id: 4, title: '信息确认', sub: '填写作业名称并提交' }
+      ],
+      // 区域类型与路线类型
+      areaTypes: [
+        {
+          value: 'loopArea',
+          label: '回字形区域',
+          desc: '规则四边形地块,适合标准往返或回字形路线',
+          routes: [{ value: 'loop', label: '回字形路线(loop)' }]
+        },
+        {
+          value: 'bowArea',
+          label: '弓子形区域',
+          desc: '一侧为弧形或不规则,适合弓字形或自适应路线',
+          routes: [{ value: 'bow', label: '弓子形路线(bow)' }]
+        },
+        {
+          value: 'customArea',
+          label: '自定义区域',
+          desc: '任意多边形地块,路线由后端自适应规划',
+          routes: [{ value: 'custom', label: '自定义路线(custom)' }]
+        },
+        {
+          value: 'ridgeArea',
+          label: '垄沟区域',
+          desc: '存在大量垄沟或等距行的地块',
+          routes: [{ value: 'ridge', label: '垄沟路线(ridge)' }]
+        }
+      ],
+      selectedAreaType: 'loopArea',
+      selectedRouteType: 'loop',
+      mode: 'area', // area | obstacle | return
+      modes: [
+        { value: 'area', label: '作业区域点' },
+        { value: 'obstacle', label: '障碍物点' },
+        { value: 'return', label: '返航点' }
+      ],
+      currentObstacle: [],
+      submitting: false,
+      locating: false,
+      // 高德地图相关
+      map: null,
+      amapLoaded: false,
+      markers: [],
+      geolocation: null, // 高德定位实例
+      obstacleMarkers: [], // 障碍物标记数组
+      returnMarker: null, // 返航点标记
+
+      // 设备实时位置轮询(用于“遥控车 + 实时打点”)
+      realtimeMarker: null,
+      realtimeTimer: null,
+      lastReportTime: null,
+      latestRealtimeLngLat: null,
+      polling: false,
+      // 回字形(loopArea) 编辑相关
+      loopMarkers: [],
+      loopPolygon: null,
+      loopReplaceIndex: null,
+      // TODO: 将下面的占位 KEY 替换为真实的高德地图 Key(仅 H5 生效)
+      amapKey: '9f2cac7ea18905dd3830cf7360a43a35',
+      jscode: '41af52e416d1fd1b15020dac066cec86',
+      jobState: {
+        machineCode: '',
+        areaType: 'loopArea',
+        routeType: 'loop',
+        areaPoints: [],
+        obstacles: [],
+        returnPoint: null,
+        startPointIndex: 0,
+        jobName: '',
+        fieldId: '',
+        pathWidth: 100
+      }
+      ,
+      // 回字形编辑相关图形对象(外圈逻辑已移除)
+      areaPolygon: null
+    }
+  },
+
+  onLoad(options) {
+    const { machineCode, id } = options || {}
+    if (machineCode) {
+      this.jobState.machineCode = machineCode 
+      this.jobState.machineId = id
+    }
+  },
+  // UniApp 页面就绪生命周期(H5 平台可在此初始化地图)
+    onReady() {
+      // 在页面就绪时再加载 AMap 脚本(确保 DOM 容器存在)
+      if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+        // eslint-disable-next-line no-console
+        console.log('[job-create] onReady - loading AMap script')
+        this.loadScript()
+      }
+    },
+
+  computed: {
+    modeLabel() {
+      const m = this.modes.find(m => m.value === this.mode)
+      return m ? m.label : ''
+    },
+    canUndo() {
+      if (this.mode === 'area') {
+        return this.jobState.areaPoints.length > 0
+      }
+      if (this.mode === 'obstacle') {
+        return this.currentObstacle.length > 0
+      }
+      if (this.mode === 'return') {
+        return !!this.jobState.returnPoint
+      }
+      return false
+    },
+    canChangeStart() {
+      return this.jobState.areaPoints.length > 0
+    },
+    currentStartDisplay() {
+      if (!this.jobState.areaPoints.length) return '-'
+      return this.jobState.startPointIndex + 1
+    },
+    // 展示当前区域类型与路线类型名称
+    areaTypeLabel() {
+      const a = this.areaTypes.find(a => a.value === this.jobState.areaType)
+      return a ? a.label : ''
+    },
+    availableRouteTypes() {
+      const a = this.areaTypes.find(a => a.value === this.selectedAreaType)
+      return a ? a.routes : []
+    },
+    routeTypeLabel() {
+      const list = this.areaTypes.reduce((acc, cur) => {
+        if (cur.routes && cur.routes.length) {
+          acc.push(...cur.routes)
+        }
+        return acc
+      }, [])
+      const r = list.find(r => r.value === this.jobState.routeType)
+      return r ? r.label : this.jobState.routeType
+    }
+  },
+
+  onUnload() {
+    // 页面销毁时停止轮询
+    this.clearRealtimePolling()
+    // 清理所有地图标记
+    this.clearAllMarkers()
+  },
+
+  methods: {
+    loadScript() { // 挂载动态js
+				// 绑定全局回调,AMap 脚本会调用 window.mapInit
+				window.mapInit = () => {
+					this._createMapWhenReady()
+				}
+
+				var script = document.createElement('script');
+				script.src = `https://webapi.amap.com/maps?v=2.0&key=${this.amapKey}&callback=mapInit`;
+				document.body.appendChild(script);
+        this.amapLoaded = true
+			},
+
+    _createMapWhenReady() {
+      const createWhenReady = () => {
+        const container = document.getElementById('mapContainer')
+        if (!container) {
+          // 容器尚未渲染,延迟重试
+          setTimeout(createWhenReady, 200)
+          return
+        }
+
+        // 创建基础地图实例并保存在组件 data.map 中
+        const defaultCenter = [113.382, 22.5211]
+
+        const createMapWithCenter = centerArr => {
+          try {
+            this.map = new AMap.Map('mapContainer', {
+              center: centerArr || defaultCenter,
+              zoom: 16
+            })
+          } catch (err) {
+            console.error('[job-create] create map failed', err)
+            return
+          }
+
+          // 添加卫星图层
+          try {
+            if (AMap.TileLayer && typeof AMap.TileLayer.Satellite === 'function') {
+              const sat = new AMap.TileLayer.Satellite()
+              this.map.add(sat)
+            }
+          } catch (layerErr) {
+            console.warn('[job-create] satellite layer failed', layerErr)
+          }
+
+          // 添加工具栏
+          try {
+            AMap.plugin('AMap.ToolBar', () => {
+              const toolbar = new AMap.ToolBar()
+              if (this.map && typeof this.map.addControl === 'function') {
+                this.map.addControl(toolbar)
+              }
+            })
+          } catch (pluginErr) {
+            console.warn('[job-create] toolbar plugin failed', pluginErr)
+          }
+
+          // 加载高德定位插件(异步加载)
+          AMap.plugin('AMap.Geolocation', () => {
+            this.geolocation = new AMap.Geolocation({
+              enableHighAccuracy: true,
+              timeout: 10000,
+              maximumAge: 0, // 不使用缓存,每次都获取最新位置
+              convert: true, // 自动偏移坐标,偏移后的坐标为高德坐标
+              showButton: false,
+              showMarker: false,
+              showCircle: false,
+              panToLocation: false,
+              zoomToAccuracy: false,
+              // 优先使用浏览器定位,失败后使用IP定位
+              noIpLocate: 0,
+              // 使用精确定位
+              GeoLocationFirst: true
+            })
+            console.log('[job-create] 高德定位插件加载完成')
+            
+            // 插件加载完成后立即尝试定位
+            this.tryAutoLocation()
+          })
+
+          // 绑定地图点击事件
+          if (this.map && typeof this.map.on === 'function') {
+            this.map.on('click', e => {
+              const lng = e.lnglat && (e.lnglat.lng || (e.lnglat.getLng && e.lnglat.getLng()))
+              const lat = e.lnglat && (e.lnglat.lat || (e.lnglat.getLat && e.lnglat.getLat()))
+              if (lng == null || lat == null) return
+              this.onMapClick({ lng, lat })
+            })
+          }
+
+          // 地图初始化完成后,如果已选择设备,则开始轮询实时位置
+          this.setupRealtimePolling()
+        }
+
+        // 创建地图(使用默认中心点)
+        console.log('[job-create] 创建地图使用默认中心点:', defaultCenter)
+        createMapWithCenter(defaultCenter)
+      }
+
+      createWhenReady()
+    },
+
+    // 自动定位(插件加载完成后调用)
+    tryAutoLocation() {
+      console.log('[job-create] 开始自动定位...')
+      
+      this._getCurrentLocation(
+        res => {
+          console.log('[job-create] 自动定位成功:', res)
+          const { longitude, latitude } = res || {}
+          if (longitude != null && latitude != null) {
+            console.log('[job-create] 使用定位坐标:', [longitude, latitude])
+            this._centerMapToLngLat(longitude, latitude, 16)
+            uni.showToast({
+              title: '已定位到您的位置',
+              icon: 'success',
+              duration: 2000
+            })
+          } else {
+            console.warn('[job-create] 定位返回坐标无效')
+          }
+        },
+        err => {
+          console.warn('[job-create] 自动定位失败:', err)
+          // 不显示错误提示,避免干扰用户
+        }
+      )
+    },
+    // 尝试获取设备定位并居中地图(优先使用 uni.getLocation,回退到 navigator 或模拟点)
+    _getDeviceLocationAndCenter() {
+      const doCenter = (lng, lat) => {
+        if (!lng && lng !== 0) return
+        if (!lat && lat !== 0) return
+        this._centerMapToLngLat(lng, lat, 18)
+        // 在当前位置添加一个简单标记(不影响现有图层)
+        try {
+          if (this.map && typeof AMap !== 'undefined' && AMap.Marker) {
+            new AMap.Marker({
+              position: [lng, lat],
+              map: this.map
+            })
+          }
+        } catch (err) {
+          // eslint-disable-next-line no-console
+          console.warn('[job-create] add marker failed', err)
+        }
+      }
+
+      // 使用高德定位
+      this._getCurrentLocation(
+        res => {
+          console.log("当前定位", res);
+          const { longitude, latitude } = res || {}
+          if (longitude != null && latitude != null) {
+            doCenter(longitude, latitude)
+          } else {
+            const p = this.getMapCenterPoint() || mockCurrentPoint(0)
+            doCenter(p.lng, p.lat)
+          }
+        },
+        () => {
+          const p = this.getMapCenterPoint() || mockCurrentPoint(0)
+          doCenter(p.lng, p.lat)
+        }
+      )
+
+      // 最后回退到地图中心或模拟点
+      const p = this.getMapCenterPoint() || mockCurrentPoint(0)
+      doCenter(p.lng, p.lat)
+    },
+
+    // 将地图居中到指定经纬度并设置缩放(安全调用)
+    _centerMapToLngLat(lng, lat, zoom = 18) {
+      if (!this.map) return
+      try {
+        if (typeof this.map.setCenter === 'function') {
+          this.map.setCenter([lng, lat])
+        }
+        if (typeof this.map.setZoom === 'function' && typeof zoom === 'number') {
+          this.map.setZoom(zoom)
+        }
+      } catch (err) {
+        // eslint-disable-next-line no-console
+        console.warn('[job-create] center map failed', err)
+      }
+    },
+    // AMap 加载由页面顶部的全局 callback `mapInit` 与 `loadScript()` 管理
+
+    // 使用高德定位获取当前位置
+    _getCurrentLocation(successCallback, failCallback, retryCount = 0) {
+      if (!this.geolocation) {
+        // 如果还未初始化,等待一段时间后重试,最多重试5次
+        if (retryCount < 5) {
+          console.log(`[job-create] 高德定位未初始化,${retryCount + 1}秒后重试...`)
+          setTimeout(() => {
+            this._getCurrentLocation(successCallback, failCallback, retryCount + 1)
+          }, 1000)
+          return
+        } else {
+          console.warn('[job-create] 高德定位未初始化,重试失败')
+          if (failCallback) failCallback(new Error('高德定位未初始化'))
+          return
+        }
+      }
+
+      console.log('[job-create] 开始调用高德定位 getCurrentPosition...')
+      
+      this.geolocation.getCurrentPosition((status, result) => {
+        console.log('[job-create] 定位回调 status:', status, 'result:', result)
+        
+        if (status === 'complete') {
+          const { lng, lat } = result.position
+          console.log('[job-create] 高德定位成功:', { lng, lat })
+          if (successCallback) {
+            successCallback({
+              longitude: lng,
+              latitude: lat
+            })
+          }
+        } else {
+          console.error('[job-create] 高德定位失败:', result)
+          let errorMsg = '定位失败'
+          let errorDetail = ''
+          
+          switch(result.info) {
+            case 'FAILED':
+              errorMsg = '定位失败,请检查网络连接'
+              errorDetail = '可能原因:网络问题或GPS信号弱'
+              break
+            case 'NOT_SUPPORTED':
+              errorMsg = '浏览器不支持定位功能'
+              errorDetail = '请使用支持地理定位的现代浏览器'
+              break
+            case 'PERMISSION_DENIED':
+              errorMsg = '定位权限被拒绝'
+              errorDetail = 'HTTPS环境下需要用户授权定位权限,HTTP环境下浏览器会直接拒绝'
+              break
+            case 'PERMISSION_GRANTED':
+              errorMsg = '定位权限已获取但定位失败'
+              errorDetail = '可能是GPS信号问题'
+              break
+            case 'TIMEOUT':
+              errorMsg = '定位请求超时'
+              errorDetail = '请检查网络连接或GPS信号'
+              break
+            default:
+              errorMsg = `定位失败: ${result.info}`
+              errorDetail = result.message || ''
+          }
+          
+          console.warn('[job-create] 定位失败详情:', errorMsg, errorDetail)
+          
+          if (failCallback) {
+            failCallback({ 
+              code: result.info, 
+              message: errorMsg,
+              detail: errorDetail
+            })
+          }
+        }
+      })
+    },
+
+    // 从地图获取中心点(回退到模拟坐标)
+    getMapCenterPoint() {
+      if (this.map && typeof this.map.getCenter === 'function') {
+        const c = this.map.getCenter()
+        // AMap 返回对象通常包含 lng/lat
+        return {
+          lng: c.lng || (c.lng === 0 ? 0 : c.getLng && c.getLng()),
+          lat: c.lat || (c.lat === 0 ? 0 : c.getLat && c.getLat()),
+          timestamp: Date.now()
+        }
+      }
+      return null
+    },
+    // ---------- end AMap ----------
+
+    // 区域类型与路线类型选择
+    selectAreaType(val) {
+      this.selectedAreaType = val
+      const a = this.areaTypes.find(item => item.value === val)
+      if (a && a.routes && a.routes.length) {
+        this.selectedRouteType = a.routes[0].value
+      }
+      this.jobState.areaType = this.selectedAreaType
+      this.jobState.routeType = this.selectedRouteType
+    },
+    selectRouteType(val) {
+      this.selectedRouteType = val
+      this.jobState.routeType = val
+    },
+    // 模式切换
+    switchMode(val) {
+      this.mode = val
+    },
+
+    // ===================== 设备实时位置轮询(参考 job-detail) =====================
+    setupRealtimePolling() {
+      // 仅 Step2 打点建模时需要实时位置
+      if (this.currentStep !== 2) return
+
+      const deviceId = this.jobState.machineId || this.jobState.machineCode
+      if (!deviceId) return
+      if (!this.amapLoaded || !this.map) return
+
+      // 避免重复开启
+      if (this.realtimeTimer) return
+
+      // 立刻拉一次
+      this.fetchRealtimeAndUpdate()
+
+      this.realtimeTimer = setInterval(() => {
+        this.fetchRealtimeAndUpdate()
+      }, 3000)
+    },
+
+    clearRealtimePolling() {
+      if (this.realtimeTimer) {
+        clearInterval(this.realtimeTimer)
+        this.realtimeTimer = null
+      }
+      this.polling = false
+    },
+
+    async fetchRealtimeAndUpdate() {
+      try {
+        const deviceId = this.jobState.machineCode
+        if (!deviceId) return
+
+        this.polling = true
+        const res = await getRealtimeData(deviceId)
+        const payload = res && res.data && (res.data.data || res.data)
+        if (!payload) return
+
+        const reportTime = payload.reportTime
+        if (reportTime && this.lastReportTime && reportTime < this.lastReportTime) {
+          return
+        }
+        if (reportTime) this.lastReportTime = reportTime
+
+        const pt = payload.currentPoint
+        if (!pt || pt.x == null || pt.y == null) return
+
+        const lngLat = [pt.x, pt.y]
+        this.latestRealtimeLngLat = lngLat
+
+        this.updateRealtimeMarker(lngLat)
+
+        // 轻量跟随:视野中心跟随车,不频繁 fitView
+        try {
+          if (this.map && typeof this.map.setCenter === 'function') {
+            this.map.setCenter(lngLat)
+          }
+        } catch (e) {}
+      } catch (e) {
+        // eslint-disable-next-line no-console
+        console.warn('[job-create] fetchRealtimeAndUpdate failed', e)
+      } finally {
+        this.polling = false
+      }
+    },
+
+    updateRealtimeMarker(lngLat) {
+      if (!this.map || !this.amapLoaded) return
+      if (!lngLat || lngLat.length !== 2) return
+
+      if (!this.realtimeMarker) {
+        this.realtimeMarker = new AMap.Marker({
+          map: this.map,
+          position: lngLat
+        })
+      } else {
+        this.realtimeMarker.setPosition(lngLat)
+      }
+    },
+
+    // 新增点(使用模拟坐标/地图中心点)
+    addPoint() {
+      // Step2 的“新增点”需要使用设备实时位置:
+      // - 轮询拿到的 latestRealtimeLngLat 优先
+      // - 回退到地图中心点(手动拖拽视野时)
+      // - 再回退到 mock
+      const realtimeLngLat = this.latestRealtimeLngLat
+      const realtimePoint = realtimeLngLat && realtimeLngLat.length === 2
+        ? { lng: realtimeLngLat[0], lat: realtimeLngLat[1], timestamp: Date.now() }
+        : null
+
+      // 回退:地图中心点
+      let centerPoint = null
+      if (this.amapLoaded && this.map) {
+        centerPoint = this.getMapCenterPoint()
+      }
+
+      const fallbackPoint = centerPoint || mockCurrentPoint(0)
+      const point = realtimePoint || fallbackPoint
+
+      if (this.mode === 'area') {
+        this.onMapClick({ lng: point.lng, lat: point.lat })
+      } else if (this.mode === 'obstacle') {
+        this.currentObstacle.push(point)
+        // 添加障碍物标记
+        this.addObstacleMarker(point, this.jobState.obstacles.length, this.currentObstacle.length - 1)
+      } else if (this.mode === 'return') {
+        if (this.jobState.returnPoint) {
+          uni.showModal({
+            title: '覆盖返航点',
+            content: '已存在返航点,是否覆盖为当前设备位置?',
+            success: res => {
+              if (res.confirm) {
+                this.jobState.returnPoint = point
+                // 更新返航点标记
+                this.updateReturnMarker(point)
+              }
+            }
+          })
+        } else {
+          this.jobState.returnPoint = point
+          // 添加返航点标记
+          this.updateReturnMarker(point)
+        }
+      }
+    },
+
+    // 撤销点
+    undoPoint() {
+      if (!this.canUndo) return
+      if (this.mode === 'area') {
+        // if (this.selectedAreaType === 'loopArea') {
+          // 删除最后一个点,同时删除对应 draggable marker
+          const lastIdx = this.jobState.areaPoints.length - 1
+          if (lastIdx >= 0) {
+            this.jobState.areaPoints.pop()
+            const m = this.loopMarkers.pop()
+            if (m && typeof m.setMap === 'function') {
+              try { m.setMap(null) } catch (e) {}
+            }
+            // 更新 polygon 或清除
+            if (this.jobState.areaPoints.length >= 3) {
+              this.recomputeLoopPolygon()
+            } else {
+              if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
+                try { this.loopPolygon.setMap(null) } catch (e) {}
+              }
+              this.loopPolygon = null
+            }
+          }
+        // } 
+        // else {
+        //   // 非回字形:删除最后一个点,并同步删除最后一个 marker + 重绘 polygon
+        //   const lastIdx = this.jobState.areaPoints.length - 1
+        //   if (lastIdx >= 0) {
+        //     this.jobState.areaPoints.pop()
+        //     const m = this.markers && this.markers.length ? this.markers.pop() : null
+        //     if (m && typeof m.setMap === 'function') {
+        //       try { m.setMap(null) } catch (e) {}
+        //     }
+        //   }
+
+        //   if (this.jobState.areaPoints.length >= 3) {
+        //     this.drawAreaPolygon()
+        //   } else if (this.areaPolygon) {
+        //     try { this.areaPolygon.setMap(null) } catch (e) {}
+        //     this.areaPolygon = null
+        //   }
+        // }
+      } else if (this.mode === 'obstacle') {
+        this.currentObstacle.pop()
+        // 删除最后一个障碍物标记
+        this.removeLastObstacleMarker()
+      } else if (this.mode === 'return') {
+        this.jobState.returnPoint = null
+        // 删除返航点标记
+        this.clearReturnMarker()
+      }
+    },
+
+    // 完成当前障碍物
+    finishCurrentObstacle() {
+      if (!this.currentObstacle.length) return
+      this.jobState.obstacles.push([...this.currentObstacle])
+      this.currentObstacle = []
+      uni.showToast({
+        title: '已保存一组障碍物',
+        icon: 'success'
+      })
+    },
+
+    // 将“录入中”的障碍物也并入 jobState.obstacles(用于步骤切换/提交前兜底)
+    flushCurrentObstacle() {
+      if (this.currentObstacle && this.currentObstacle.length) {
+        this.jobState.obstacles.push([...this.currentObstacle])
+        this.currentObstacle = []
+      }
+    },
+
+    // 步骤导航
+    prevStep() {
+      if (this.currentStep === 1) return
+      this.currentStep -= 1
+
+      // 离开 Step2 时停止轮询,避免后台持续请求
+      if (this.currentStep !== 2) {
+        this.clearRealtimePolling()
+      }
+    },
+
+    nextStep() {
+      // Step1 校验:区域类型与路线类型必选
+      if (this.currentStep === 1) {
+        if (!this.selectedAreaType || !this.selectedRouteType) {
+          uni.showToast({
+            title: '请选择作业区域类型和路线类型',
+            icon: 'none'
+          })
+          return
+        }
+        this.jobState.areaType = this.selectedAreaType
+        this.jobState.routeType = this.selectedRouteType
+      }
+      // Step2 校验:作业区域点 / 返航点
+      if (this.currentStep === 2) {
+        if (this.jobState.areaPoints.length < 3) {
+          uni.showToast({
+            title: '作业区域点至少需要 3 个',
+            icon: 'none'
+          })
+          return
+        }
+
+        // 离开打点页前,把“录入中”的障碍物也保存下来,避免忘记点“完成当前障碍物”
+        this.flushCurrentObstacle()
+
+        if (!this.jobState.returnPoint) {
+          uni.showToast({
+            title: '请先设置返航点',
+            icon: 'none'
+          })
+          return
+        }
+      }
+      // Step3 校验:需存在作业区域点
+      if (this.currentStep === 3) {
+        if (!this.jobState.areaPoints.length) {
+          uni.showToast({
+            title: '请先在上一步记录作业区域点',
+            icon: 'none'
+          })
+          return
+        }
+      }
+      if (this.currentStep < 4) {
+        this.currentStep += 1
+
+        // 进入 Step2 时开启轮询(地图可能已初始化)
+        if (this.currentStep === 2) {
+          this.setupRealtimePolling()
+        } else {
+          // 离开 Step2 时关闭轮询
+          this.clearRealtimePolling()
+        }
+      }
+    },
+
+    // 起点选择
+    setStartIndex(index) {
+      if (!this.jobState.areaPoints.length) return
+      this.jobState.startPointIndex = index
+    },
+    prevStart() {
+      if (!this.canChangeStart) return
+      const total = this.jobState.areaPoints.length
+      this.jobState.startPointIndex =
+        (this.jobState.startPointIndex - 1 + total) % total
+    },
+    nextStart() {
+      if (!this.canChangeStart) return
+      const total = this.jobState.areaPoints.length
+      this.jobState.startPointIndex =
+        (this.jobState.startPointIndex + 1) % total
+    },
+
+    // 手动定位
+    async manualLocation() {
+      if (this.locating) return
+      this.locating = true
+
+      const doCenter = (lng, lat) => {
+        if (!lng && lng !== 0) return
+        if (!lat && lat !== 0) return
+        this._centerMapToLngLat(lng, lat, 18)
+        uni.showToast({
+          title: '已定位到您的位置',
+          icon: 'success',
+          duration: 2000
+        })
+      }
+
+      try {
+        // 检查是否为HTTPS环境
+        const isSecureContext = window.isSecureContext || window.location.protocol === 'https:'
+        if (!isSecureContext) {
+          console.warn('[job-create] 非HTTPS环境,浏览器可能拒绝定位请求')
+          uni.showModal({
+            title: '定位提示',
+            content: '当前为HTTP环境,浏览器可能拒绝定位请求。建议使用HTTPS访问或使用IP定位。',
+            showCancel: false
+          })
+        }
+
+        // 使用高德定位
+        await new Promise((resolve, reject) => {
+          this._getCurrentLocation(
+            res => {
+              const { longitude, latitude } = res || {}
+              if (longitude != null && latitude != null) {
+                doCenter(longitude, latitude)
+                resolve()
+              } else {
+                reject(new Error('无效坐标'))
+              }
+            },
+            reject
+          )
+        })
+      } catch (err) {
+        console.error('[job-create] 手动定位失败:', err)
+        let errorMsg = '定位失败'
+        let showDetail = false
+        
+        if (err.message) {
+          errorMsg = err.message
+        }
+        if (err.detail) {
+          showDetail = true
+        }
+        
+        if (showDetail) {
+          uni.showModal({
+            title: errorMsg,
+            content: err.detail || '请检查浏览器定位权限设置,或确保使用HTTPS访问',
+            showCancel: false
+          })
+        } else {
+          uni.showToast({
+            title: errorMsg,
+            icon: 'none',
+            duration: 3000
+          })
+        }
+      } finally {
+        this.locating = false
+      }
+    },
+
+    // 提交作业
+    async submitJob() {
+      if (!this.jobState.jobName) {
+        uni.showToast({
+          title: '请填写作业名称',
+          icon: 'none'
+        })
+        return
+      }
+      if (!this.jobState.fieldId) {
+        uni.showToast({
+          title: '请填写地块ID',
+          icon: 'none'
+        })
+        return
+      }
+      if (!this.jobState.pathWidth) {
+        uni.showToast({
+          title: '请填写路径宽度',
+          icon: 'none'
+        })
+        return
+      }
+      if (this.jobState.areaPoints.length < 3) {
+        uni.showToast({
+          title: '作业区域点至少需要 3 个',
+          icon: 'none'
+        })
+        return
+      }
+
+      // 提交前兜底:把“录入中”的障碍物也并入 obstacles
+      this.flushCurrentObstacle()
+
+      // 将高德坐标转换为WGS84坐标
+      const convertedAreaPoints = coordinateUtils.convertPointsToWgs84(this.jobState.areaPoints)
+      const convertedObstacles = this.jobState.obstacles.map(obstacleGroup =>
+        coordinateUtils.convertPointsToWgs84(obstacleGroup)
+      )
+      const convertedReturnPoint = this.jobState.returnPoint ?
+        coordinateUtils.convertPointToWgs84(this.jobState.returnPoint) : undefined
+
+      // 映射 areaType 到数字
+      const areaTypeMap = {
+        'loopArea': 1,   // 回字形
+        'bowArea': 2,    // 弓字形
+        'customArea': 3, // 自定义
+        'ridgeArea': 4   // 垄沟
+      }
+      console.log("this.jobState",this.jobState);
+      const payload = {
+        // deviceId: this.jobState.machineCode,
+        deviceId: this.jobState.machineId,
+        fieldId: parseInt(this.jobState.fieldId),
+        taskName: this.jobState.jobName,
+        areaType: areaTypeMap[this.jobState.areaType] || 1,
+        waypoints: convertedAreaPoints.map(p => ({ lng: p.lng, lat: p.lat })),
+        obstacles: convertedObstacles.flat().map(p => ({ lng: p.lng, lat: p.lat })),
+        returnPoint: convertedReturnPoint ? { lng: convertedReturnPoint.lng, lat: convertedReturnPoint.lat } : undefined,
+        pathWidth: parseInt(this.jobState.pathWidth)
+      }
+      console.log("payload",payload);
+      
+
+      this.submitting = true
+      uni.showLoading({ title: '提交中...' })
+      try {
+        const res = await createJob(payload)
+        console.log("res收到尽快发货",res);
+        
+        const { data } = res || {}
+        if (data && data.code === 200) {
+          uni.showToast({
+            title: '作业创建成功',
+            icon: 'success'
+          })
+          setTimeout(() => {
+            uni.navigateBack()
+          }, 800)
+        } else {
+          uni.showToast({
+            title: (data && data.msg) || '提交失败',
+            icon: 'none'
+          })
+        }
+      } catch (err) {
+        console.error('创建作业失败', err)
+        uni.showToast({
+          title: '网络异常或接口未就绪',
+          icon: 'none'
+        })
+      } finally {
+        this.submitting = false
+        uni.hideLoading()
+      }
+    },
+
+    // 区域编辑逻辑(支持所有区域类型)
+    // - 点击地图依次记录点(或替换已选点)
+    // - 回字形区域使用可拖拽 Marker,其他区域使用普通 Marker
+    // - 点击某个点可以进入“替换模式”,下一次地图点击会替换该点
+    onMapClick({ lng, lat } = {}) {
+      if (!lng && lng !== 0) return
+      if (!lat && lat !== 0) return
+      
+      // 确保在区域编辑模式下
+      if (this.mode !== 'area') return
+
+      const newPoint = { lng, lat, timestamp: Date.now() }
+
+      // 回字形区域特殊处理(支持拖拽和替换)
+    
+        // 替换模式下替换指定点
+        if (this.loopReplaceIndex != null && this.loopReplaceIndex >= 0 && this.loopReplaceIndex < this.jobState.areaPoints.length) {
+          this.jobState.areaPoints.splice(this.loopReplaceIndex, 1, newPoint)
+          const m = this.loopMarkers[this.loopReplaceIndex]
+          if (m && typeof m.setPosition === 'function') {
+            try { m.setPosition([lng, lat]) } catch (e) {}
+          }
+          this.loopReplaceIndex = null
+          if (this.jobState.areaPoints.length >= 3) this.recomputeLoopPolygon()
+          return
+        }
+        
+        // 正常添加点
+        this.jobState.areaPoints.push(newPoint)
+        const idx = this.jobState.areaPoints.length - 1
+        this.createOrUpdateLoopMarker(idx, newPoint)
+        if (this.jobState.areaPoints.length >= 3) {
+          this.recomputeLoopPolygon()
+        }
+      // }
+    },
+    
+
+    createOrUpdateLoopMarker(index, point) {
+      
+      if (!this.map || typeof AMap === 'undefined') return
+      const pos = [point.lng, point.lat]
+      let marker = this.loopMarkers[index]
+      if (marker) {
+        try { marker.setPosition(pos) } catch (e) {}
+        return
+      }
+      // Use smaller icon size
+      let icon = null
+      try {
+        icon = new AMap.Icon({
+          image: "static/icons/poi-marker-default.png",
+          size: new AMap.Size(20, 28),
+          imageSize: new AMap.Size(20, 28)
+        })
+      } catch (e) {
+        icon = "static/icons/poi-marker-default.png"
+      }
+      marker = new AMap.Marker({
+        position: pos,
+        map: this.map,
+        draggable: true,
+        icon,
+        offset: new AMap.Pixel(-10, -28)
+      })
+      // add simple label showing index (P1, P2...)
+      try {
+        const labelContent = `<div class="p-marker-label">P${index + 1}</div>`
+        if (typeof marker.setLabel === 'function') {
+          marker.setLabel({
+            content: labelContent,
+            offset: new AMap.Pixel(10, -36)
+          })
+        } else {
+          marker.label = { content: labelContent }
+        }
+      } catch (e) {}
+      // drag end -> update point and polygon
+      marker.on('dragend', e => {
+        const p = marker.getPosition()
+        const lngv = p.lng || (p.getLng && p.getLng())
+        const latv = p.lat || (p.getLat && p.getLat())
+        if (lngv == null || latv == null) return
+        // Use Vue.set / $set to ensure reactivity for array index assignment
+        if (typeof this.$set === 'function') {
+          this.$set(this.jobState.areaPoints, index, { lng: lngv, lat: latv, timestamp: Date.now() })
+        } else {
+          this.jobState.areaPoints[index] = { lng: lngv, lat: latv, timestamp: Date.now() }
+        }
+        if (this.jobState.areaPoints.length >= 3) this.recomputeLoopPolygon()
+      })
+      // click -> enter replace mode for this index
+      marker.on('click', () => {
+        this.loopReplaceIndex = index
+        uni.showToast({ title: `已选中 P${index + 1},下一次点击将替换该点`, icon: 'none' })
+      })
+      this.loopMarkers[index] = marker
+      // refresh labels for all markers to keep numbering consistent
+      this.refreshLoopMarkerLabels()
+    },
+
+    recomputeLoopPolygon() {
+      if (!this.map) return
+      if (!this.jobState.areaPoints || this.jobState.areaPoints.length < 3) {
+        if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
+          try { this.loopPolygon.setMap(null) } catch (e) {}
+        }
+        this.loopPolygon = null
+        return
+      }
+
+      // Outer path: ensure order as [ [lng,lat], ... ]
+      const outer = this.jobState.areaPoints.map(p => [p.lng, p.lat])
+
+      try {
+        if (this.loopPolygon) {
+          this.loopPolygon.setPath(outer)
+        } else {
+          this.loopPolygon = new AMap.Polygon({
+            map: this.map,
+            path: outer,
+            strokeColor: '#3bb44a',
+            strokeWeight: 2,
+            fillColor: '#3bb44a',
+            fillOpacity: 0.15
+          })
+        }
+      } catch (err) {
+        // eslint-disable-next-line no-console
+        console.warn('[job-create] recompute loop polygon failed', err)
+      }
+    },
+
+    refreshLoopMarkerLabels() {
+      if (!this.loopMarkers || !this.loopMarkers.length) return
+      this.loopMarkers.forEach((m, i) => {
+        try {
+          const content = `<div class="p-marker-label">P${i + 1}</div>`
+          if (typeof m.setLabel === 'function') {
+            m.setLabel({ content, offset: new AMap.Pixel(10, -36) })
+          } else if (m.label) {
+            m.label.content = content
+          }
+        } catch (e) {}
+      })
+    },
+
+    // 清空所有 loop 图形(用于删除区域)
+    clearLoopGraphics() {
+      if (this.loopMarkers && this.loopMarkers.length) {
+        this.loopMarkers.forEach(m => {
+          try { m.setMap(null) } catch (e) {}
+        })
+      }
+      this.loopMarkers = []
+      if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
+        try { this.loopPolygon.setMap(null) } catch (e) {}
+      }
+      this.loopPolygon = null
+      this.loopReplaceIndex = null
+    },
+
+    // 删除某一个点(根据需求:删除后清空整个区域)
+    deleteAreaPoints() {
+      this.jobState.areaPoints = []
+      this.clearLoopGraphics()
+    },
+
+    // 辅助显示
+    formatPoint(p) {
+      if (!p) return '--'
+      return `${p.lng.toFixed(5)}, ${p.lat.toFixed(5)}`
+    },
+    formatPointTime(ts) {
+      if (!ts) return ''
+      const d = new Date(ts)
+      const h = `${d.getHours()}`.padStart(2, '0')
+      const m = `${d.getMinutes()}`.padStart(2, '0')
+      const s = `${d.getSeconds()}`.padStart(2, '0')
+      return `${h}:${m}:${s}`
+    },
+
+    // 添加障碍物标记
+    addObstacleMarker(point, obstacleGroupIndex, pointIndex) {
+      if (!this.map || !this.amapLoaded || typeof AMap === 'undefined') return
+      
+      try {
+        const pos = [point.lng, point.lat]
+        
+        // 创建障碍物图标(使用不同颜色区分)
+        let icon = null
+        try {
+          icon = new AMap.Icon({
+            image: "static/icons/poi-marker-default.png",
+            size: new AMap.Size(16, 22),
+            imageSize: new AMap.Size(16, 22)
+          })
+        } catch (e) {
+          icon = "static/icons/poi-marker-default.png"
+        }
+
+        const marker = new AMap.Marker({
+          position: pos,
+          map: this.map,
+          icon,
+          offset: new AMap.Pixel(-8, -22)
+        })
+
+        // 添加标签显示障碍物编号
+        try {
+          const labelContent = `<div class="obstacle-marker-label">O${obstacleGroupIndex + 1}-${pointIndex + 1}</div>`
+          if (typeof marker.setLabel === 'function') {
+            marker.setLabel({
+              content: labelContent,
+              offset: new AMap.Pixel(8, -28)
+            })
+          }
+        } catch (e) {}
+
+        // 保存标记到数组
+        if (!this.obstacleMarkers[obstacleGroupIndex]) {
+          this.obstacleMarkers[obstacleGroupIndex] = []
+        }
+        this.obstacleMarkers[obstacleGroupIndex].push(marker)
+      } catch (err) {
+        console.warn('[job-create] add obstacle marker failed', err)
+      }
+    },
+
+    // 删除最后一个障碍物标记
+    removeLastObstacleMarker() {
+      const currentGroupIndex = this.jobState.obstacles.length
+      if (this.obstacleMarkers[currentGroupIndex] && this.obstacleMarkers[currentGroupIndex].length > 0) {
+        const marker = this.obstacleMarkers[currentGroupIndex].pop()
+        if (marker && typeof marker.setMap === 'function') {
+          try {
+            marker.setMap(null)
+          } catch (e) {}
+        }
+      }
+    },
+
+    // 更新返航点标记
+    updateReturnMarker(point) {
+      if (!this.map || !this.amapLoaded || typeof AMap === 'undefined') return
+      
+      try {
+        const pos = [point.lng, point.lat]
+        
+        // 如果已存在返航点标记,先删除
+        this.clearReturnMarker()
+
+        // 创建返航点图标(使用特殊颜色)
+        let icon = null
+        try {
+          icon = new AMap.Icon({
+            image: "static/icons/poi-marker-default.png",
+            size: new AMap.Size(20, 28),
+            imageSize: new AMap.Size(20, 28)
+          })
+        } catch (e) {
+          icon = "static/icons/poi-marker-default.png"
+        }
+
+        this.returnMarker = new AMap.Marker({
+          position: pos,
+          map: this.map,
+          icon,
+          offset: new AMap.Pixel(-10, -28)
+        })
+
+        // 添加标签
+        try {
+          const labelContent = '<div class="return-marker-label">返航点</div>'
+          if (typeof this.returnMarker.setLabel === 'function') {
+            this.returnMarker.setLabel({
+              content: labelContent,
+              offset: new AMap.Pixel(10, -36)
+            })
+          }
+        } catch (e) {}
+      } catch (err) {
+        console.warn('[job-create] update return marker failed', err)
+      }
+    },
+
+    // 清除返航点标记
+    clearReturnMarker() {
+      if (this.returnMarker && typeof this.returnMarker.setMap === 'function') {
+        try {
+          this.returnMarker.setMap(null)
+        } catch (e) {}
+      }
+      this.returnMarker = null
+    },
+
+    // 清理所有标记(页面卸载时调用)
+    clearAllMarkers() {
+      // 清理作业区域标记
+      if (this.loopMarkers && this.loopMarkers.length) {
+        this.loopMarkers.forEach(m => {
+          try {
+            if (m && typeof m.setMap === 'function') {
+              m.setMap(null)
+            }
+          } catch (e) {}
+        })
+        this.loopMarkers = []
+      }
+
+      // 清理障碍物标记
+      if (this.obstacleMarkers && this.obstacleMarkers.length) {
+        this.obstacleMarkers.forEach(group => {
+          if (group && group.length) {
+            group.forEach(m => {
+              try {
+                if (m && typeof m.setMap === 'function') {
+                  m.setMap(null)
+                }
+              } catch (e) {}
+            })
+          }
+        })
+        this.obstacleMarkers = []
+      }
+
+      // 清理返航点标记
+      this.clearReturnMarker()
+
+      // 清理实时位置标记
+      if (this.realtimeMarker && typeof this.realtimeMarker.setMap === 'function') {
+        try {
+          this.realtimeMarker.setMap(null)
+        } catch (e) {}
+        this.realtimeMarker = null
+      }
+
+      // 清理多边形
+      if (this.loopPolygon && typeof this.loopPolygon.setMap === 'function') {
+        try {
+          this.loopPolygon.setMap(null)
+        } catch (e) {}
+        this.loopPolygon = null
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.page {
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  background-color: #f6f9f7;
+}
+
+.header {
+  padding: 24rpx 28rpx 12rpx;
+}
+
+.title-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: baseline;
+  margin-bottom: 12rpx;
+}
+
+.title {
+  font-size: 34rpx;
+  font-weight: 600;
+  color: #2c3e50;
+}
+
+.device-id {
+  font-size: 24rpx;
+  color: #8c9396;
+}
+
+.step-row {
+  display: flex;
+  padding: 10rpx 8rpx;
+  border-radius: 16rpx;
+  background-color: #eef6f0;
+}
+
+.step-item {
+  flex: 1;
+  display: flex;
+  align-items: center;
+}
+
+.step-index {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 18rpx;
+  border: 2rpx solid #b0c4b8;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 22rpx;
+  color: #7f8c8d;
+  margin-right: 10rpx;
+}
+
+.step-index.active {
+  background: linear-gradient(135deg, #3bb44a, #66cc6a);
+  color: #ffffff;
+  border-color: transparent;
+}
+
+.step-index.done {
+  background-color: #ffffff;
+  color: #3bb44a;
+  border-color: #3bb44a;
+}
+
+.step-texts {
+  display: flex;
+  flex-direction: column;
+}
+
+.step-title {
+  font-size: 24rpx;
+  color: #2c3e50;
+}
+
+.step-sub {
+  font-size: 20rpx;
+  color: #8c9396;
+}
+
+.content {
+  flex: 1;
+  padding: 10rpx 24rpx 130rpx;
+  box-sizing: border-box;
+}
+
+.step-block {
+  display: flex;
+  flex-direction: column;
+  gap: 18rpx;
+}
+
+/* 区域类型 & 路线类型选择卡片 */
+.card.select-card {
+  background-color: #ffffff;
+  border-radius: 20rpx;
+  padding: 20rpx 22rpx 16rpx;
+  box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
+}
+
+.select-title {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #2c3e50;
+}
+
+.select-sub {
+  margin-top: 6rpx;
+  font-size: 22rpx;
+  color: #8c9396;
+  line-height: 1.5;
+}
+
+.select-tabs {
+  margin-top: 14rpx;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+
+.select-tab {
+  min-width: 46%;
+  padding: 14rpx 16rpx;
+  border-radius: 16rpx;
+  background-color: #f3f5f7;
+  box-sizing: border-box;
+}
+
+.select-tab.small {
+  min-width: 30%;
+}
+
+.select-tab-label {
+  font-size: 24rpx;
+  color: #2c3e50;
+}
+
+.select-tab-sub {
+  margin-top: 4rpx;
+  font-size: 20rpx;
+  color: #8c9396;
+}
+
+.select-tab.active {
+  background: linear-gradient(135deg, #3bb44a, #66cc6a);
+  box-shadow: 0 4rpx 12rpx rgba(59, 180, 74, 0.26);
+}
+
+.select-tab.active .select-tab-label,
+.select-tab.active .select-tab-sub {
+  color: #ffffff;
+}
+
+.map-card {
+  background-color: #ffffff;
+  border-radius: 20rpx;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
+  overflow: hidden;
+}
+
+.map-card.small .map-body {
+  height: 260rpx;
+}
+
+.map-header {
+  padding: 20rpx 22rpx 10rpx;
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.map-header-actions {
+  margin-left: 20rpx;
+}
+
+.map-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #2c3e50;
+}
+
+.map-sub {
+  margin-top: 4rpx;
+  font-size: 22rpx;
+  color: #8c9396;
+}
+
+.map-body {
+  height: 320rpx;
+  background-color: #d3dce6;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.map-placeholder {
+  font-size: 24rpx;
+  color: #ffffff;
+  text-align: center;
+  white-space: pre-line;
+  width: 100%;
+}
+
+/* AMap 容器样式(确保占满 map-body) */
+#mapContainer {
+  width: 100%;
+  height: 100%;
+}
+
+.map-footer {
+  padding: 12rpx 18rpx 16rpx;
+  background-color: #f7faf8;
+}
+
+.map-hint {
+  font-size: 22rpx;
+  color: #8c9396;
+}
+
+.mode-card {
+  background-color: #ffffff;
+  border-radius: 20rpx;
+  padding: 18rpx 20rpx 14rpx;
+  box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
+}
+
+.mode-title-row {
+  margin-bottom: 10rpx;
+}
+
+.mode-title {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #2c3e50;
+}
+
+.mode-sub {
+  margin-top: 4rpx;
+  font-size: 22rpx;
+  color: #8c9396;
+}
+
+.mode-tabs {
+  display: flex;
+  gap: 12rpx;
+}
+
+.mode-tab {
+  flex: 1;
+  padding: 12rpx 0;
+  border-radius: 30rpx;
+  background-color: #f3f5f7;
+  text-align: center;
+  font-size: 24rpx;
+  color: #666;
+}
+
+.mode-tab.active {
+  background: linear-gradient(135deg, #3bb44a, #66cc6a);
+  color: #ffffff;
+  box-shadow: 0 4rpx 12rpx rgba(59, 180, 74, 0.3);
+}
+
+.panel-card {
+  background-color: #ffffff;
+  border-radius: 20rpx;
+  padding: 18rpx 20rpx 14rpx;
+  box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
+}
+
+.panel-row {
+  display: flex;
+  gap: 12rpx;
+}
+
+.panel-row.center {
+  justify-content: space-between;
+  align-items: center;
+}
+
+.panel-desc {
+  margin-top: 8rpx;
+  font-size: 22rpx;
+  color: #8c9396;
+}
+
+.list-card {
+  background-color: #ffffff;
+  border-radius: 20rpx;
+  padding: 18rpx 20rpx 10rpx;
+  box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.03);
+  max-height: 420rpx;
+}
+
+.list-title-row {
+  margin-bottom: 6rpx;
+}
+
+.list-title {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #2c3e50;
+}
+
+.list-scroll {
+  max-height: 360rpx;
+}
+
+.list-group {
+  margin-top: 10rpx;
+}
+
+.list-group-header {
+  margin-bottom: 4rpx;
+}
+
+.list-group-title {
+  font-size: 24rpx;
+  color: #555;
+}
+
+.point-list {
+  margin-top: 4rpx;
+}
+
+.point-item {
+  padding: 8rpx 4rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+  display: flex;
+  align-items: center;
+}
+
+.point-item.small {
+  padding-left: 20rpx;
+}
+
+.point-item.active {
+  background-color: #f0f9f2;
+}
+
+.point-label {
+  font-size: 22rpx;
+  color: #3bb44a;
+  margin-right: 8rpx;
+}
+
+.point-coord {
+  flex: 1;
+  font-size: 22rpx;
+  color: #333;
+}
+
+.point-time {
+  font-size: 20rpx;
+  color: #999;
+}
+
+.obstacle-item {
+  margin-top: 6rpx;
+}
+
+.obstacle-title {
+  font-size: 22rpx;
+  color: #666;
+  margin-bottom: 2rpx;
+}
+
+.empty-row {
+  padding: 12rpx 0;
+  font-size: 22rpx;
+  color: #999;
+}
+
+.start-index {
+  flex: 1;
+  align-items: center;
+  justify-content: center;
+  display: flex;
+}
+
+.start-index-text {
+  font-size: 26rpx;
+  color: #2c3e50;
+}
+
+.card.confirm-card {
+  padding: 22rpx 24rpx 10rpx;
+}
+
+.confirm-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #2c3e50;
+  margin-bottom: 12rpx;
+}
+
+.form-item {
+  margin-bottom: 16rpx;
+}
+
+.form-item.required .label::after {
+  content: '*';
+  color: #ff4d4f;
+  margin-left: 4rpx;
+}
+
+.label {
+  display: block;
+  font-size: 26rpx;
+  color: #555;
+  margin-bottom: 8rpx;
+}
+
+.input {
+  width: 100%;
+  height: 80rpx;
+  line-height: 80rpx;
+  padding: 14rpx 18rpx;
+  border-radius: 12rpx;
+  background-color: #f7f7f7;
+  font-size: 26rpx;
+  color: #333;
+  box-sizing: border-box;
+}
+
+.summary-row {
+  display: flex;
+  justify-content: space-between;
+  padding: 8rpx 0;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+
+.summary-label {
+  font-size: 24rpx;
+  color: #777;
+}
+
+.summary-value {
+  font-size: 24rpx;
+  color: #333;
+}
+
+.tips-card {
+  padding: 20rpx 22rpx 16rpx;
+}
+
+.tips-title {
+  font-size: 26rpx;
+  color: #2c3e50;
+  font-weight: 600;
+  margin-bottom: 6rpx;
+}
+
+.tips-content text {
+  display: block;
+  font-size: 22rpx;
+  color: #666;
+  margin-top: 4rpx;
+}
+
+.footer {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 12rpx 26rpx 24rpx;
+  background-color: #ffffff;
+  box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
+  display: flex;
+  gap: 12rpx;
+  box-sizing: border-box;
+}
+
+.btn {
+  flex: 1;
+  height: 84rpx;
+  line-height: 84rpx;
+  border-radius: 42rpx;
+  font-size: 28rpx;
+}
+
+.btn.primary {
+  background: linear-gradient(135deg, #3bb44a, #66cc6a);
+  color: #ffffff;
+}
+
+.btn.ghost {
+  background-color: #f4f5f7;
+  color: #555;
+}
+
+.btn:disabled {
+  opacity: 0.5;
+}
+
+.btn-location {
+  padding: 8rpx 16rpx;
+  border-radius: 20rpx;
+  background-color: #f0f9f2;
+  border: 1rpx solid #3bb44a;
+  font-size: 24rpx;
+  color: #3bb44a;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-width: 120rpx;
+  height: 56rpx;
+  box-sizing: border-box;
+}
+
+.btn-location-text {
+  font-size: 24rpx;
+  color: #3bb44a;
+}
+
+/* 任务标题行新增作业按钮样式(与详情页保持风格一致) */
+.task-title-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.task-title-left {
+  display: flex;
+  flex-direction: column;
+}
+
+.task-title-text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #2c3e50;
+}
+
+.task-title-sub {
+  margin-top: 4rpx;
+  font-size: 22rpx;
+  color: #8c9396;
+}
+
+.task-add-btn {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 30rpx;
+  background: linear-gradient(135deg, #3bb44a, #66cc6a);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 4rpx 12rpx rgba(59, 180, 74, 0.3);
+}
+
+.task-add-plus {
+  font-size: 40rpx;
+  color: #ffffff;
+  line-height: 1;
+}
+
+.p-marker-label {
+  background: rgba(59,180,74,0.95);
+  color: #fff;
+  padding: 2px 6px;
+  border-radius: 10px;
+  font-size: 18rpx;
+  line-height: 1;
+}
+
+.obstacle-marker-label {
+  background: rgba(255, 152, 0, 0.95);
+  color: #fff;
+  padding: 2px 6px;
+  border-radius: 10px;
+  font-size: 16rpx;
+  line-height: 1;
+}
+
+.return-marker-label {
+  background: rgba(33, 150, 243, 0.95);
+  color: #fff;
+  padding: 2px 8px;
+  border-radius: 10px;
+  font-size: 18rpx;
+  line-height: 1;
+  font-weight: 600;
+}
+</style>
+
+

+ 434 - 440
pages/device/job-detail/index.vue

@@ -48,485 +48,480 @@
   </view>
 </template>
 
-<script>
+<script setup>
+import { ref, computed } from 'vue'
 import { getInfo, pauseTask, startTask, stopTask, recallTask, getRealtimeData } from '@/api/services/job.js'
-import coordinateUtils from '@/utils/coordinateUtils.js'
-
-export default {
-  data() {
-    return {
-      jobId: null,
-      deviceId:null,
-      jobInfo: {},
-      map: null,
-      deviceName:'', // 设备名称
-      // 地图元素
-      areaPolygon: null, // 作业区域
-      routePolyline: null, // 规划路线
-      passedPolyline: null, // 已走路线
-      deviceMarker: null, // 设备图标
-      // 状态
-      amapLoaded: false,
-      pollingTimer: null,
-      realtimeTrackPoints: [], // 实时轨迹点:[[lng,lat], ...]
-      lastReportTime: null,
-      // 模拟数据
-      jobProgress: 0,
-      deviceSpeed: 0.0,
-      batteryLevel: 0,
-      // 字典
-      statusMap: {
-        0: '未开始',
-        1: '进行中',
-        2: '已完成',
-        3: '已暂停',
-        4: '已停止',
-        5: '已取消',
-        default: '未知'
-      },
-      areaTypeMap: {
-        1: '回字形',
-        2: '弓字形',
-        3: '自定义',
-        4: '垄沟',
-        default: '未知区域'
-      }
-    }
-  },
-  computed: {
-    jobStatus() {
-      return this.jobInfo.taskStatus
-    },
-    statusText() {
-      return this.statusMap[this.jobStatus] || this.statusMap.default
-    },
-    statusClass() {
-      return `status-${this.jobStatus}`
-    },
-    areaTypeText() {
-      return this.areaTypeMap[this.jobInfo.workAreas && this.jobInfo.workAreas.areaType || 0] || this.areaTypeMap.default
-    },
-    // 按钮可用状态
-    canPause() {
-      return this.jobStatus === 1
-    },
-    canResume() {
-      return this.jobStatus === 3
-    },
-    canStop() {
-      return [0, 1, 3].includes(this.jobStatus)
-    },
-    canRecall() {
-      return [0, 1, 3].includes(this.jobStatus)
-    }
-  },
-  onLoad(options) {
-    if (options.id || options.deviceId) {
-      this.jobId = options.id
-      this.deviceId = options.deviceId
-      this.deviceName = options.deviceName
-      this.loadJobDetails()
+
+// Props from route params (will be set in onLoad)
+const jobId = ref(null)
+const deviceId = ref(null)
+const deviceName = ref('') // 设备名称
+const jobInfo = ref({})
+
+// 地图相关
+const map = ref(null)
+const areaPolygon = ref(null) // 作业区域
+const routePolyline = ref(null) // 规划路线
+const passedPolyline = ref(null) // 已走路线
+const deviceMarker = ref(null) // 设备图标
+
+// 状态
+const amapLoaded = ref(false)
+const pollingTimer = ref(null)
+const realtimeTrackPoints = ref([]) // 实时轨迹点:[[lng,lat], ...]
+const lastReportTime = ref(null)
+
+// 模拟数据
+const jobProgress = ref(0)
+const deviceSpeed = ref(0.0)
+const batteryLevel = ref(0)
+
+// 字典
+const statusMap = {
+  0: '未开始',
+  1: '进行中',
+  2: '已完成',
+  3: '已暂停',
+  4: '已停止',
+  5: '已取消',
+  default: '未知'
+}
+
+const areaTypeMap = {
+  1: '回字形',
+  2: '弓字形',
+  3: '自定义',
+  4: '垄沟',
+  default: '未知区域'
+}
+
+// Computed properties
+const jobStatus = computed(() => jobInfo.value.taskStatus)
+const statusText = computed(() => statusMap[jobStatus.value] || statusMap.default)
+const statusClass = computed(() => `status-${jobStatus.value}`)
+const areaTypeText = computed(() => {
+  const areaType = jobInfo.value.workAreas && jobInfo.value.workAreas.areaType || 0
+  return areaTypeMap[areaType] || areaTypeMap.default
+})
+
+// 按钮可用状态
+const canPause = computed(() => jobStatus.value === 1)
+const canResume = computed(() => jobStatus.value === 3)
+const canStop = computed(() => [0, 1, 3].includes(jobStatus.value))
+const canRecall = computed(() => [0, 1, 3].includes(jobStatus.value))
+
+// Lifecycle hooks
+const onLoad = (options) => {
+  if (options.id || options.deviceId) {
+    jobId.value = options.id
+    deviceId.value = options.deviceId
+    deviceName.value = options.deviceName
+    loadJobDetails()
+  } else {
+    uni.showToast({ title: '缺少作业ID', icon: 'none' })
+    uni.navigateBack()
+  }
+}
+
+const onReady = () => {
+  initMap()
+}
+
+const onUnload = () => {
+  clearPolling()
+}
+
+// Methods
+const loadJobDetails = async () => {
+  uni.showLoading({ title: '加载中...' })
+  try {
+    const res = await getInfo(jobId.value)
+    if (res.data.code === 200 &&res.data.data.workAreas) {
+      jobInfo.value = res.data.data || {}
+      
+      updateMapElements()
+      setupPolling()
     } else {
-      uni.showToast({ title: '缺少作业ID', icon: 'none' })
-      uni.navigateBack()
+      throw new Error(res.data.msg || '加载失败')
     }
-  },
-  onReady() {
-    this.initMap()
-  },
-  onUnload() {
-    this.clearPolling()
-  },
-  methods: {
-    // 加载作业详情
-    async loadJobDetails() {
-      uni.showLoading({ title: '加载中...' })
-      try {
-        const res = await getInfo(this.jobId)
-        if (res.data.code === 200 &&res.data.data.workAreas) {
-          this.jobInfo = res.data.data || {}
-          // 更新模拟数据
-          // this.jobProgress = this.jobInfo.progress || 0
-          // this.deviceSpeed = this.jobInfo.speed || 0.0
-          // this.batteryLevel = this.jobInfo.battery || 0
-          
-          this.updateMapElements()
-          this.setupPolling()
-        } else {
-          throw new Error(res.data.msg || '加载失败')
-        }
-      } catch (err) {
-        uni.showToast({ title: err.message, icon: 'none' })
-      } finally {
-        uni.hideLoading()
-      }
-    },
-    // 初始化地图
-    initMap() {
-      if (window.AMap) {
-        this.createMap()
-      } else {
-        const script = document.createElement('script')
-        script.src = `https://webapi.amap.com/maps?v=2.0&key=9f2cac7ea18905dd3830cf7360a43a35`
-        script.onload = this.createMap
-        document.head.appendChild(script)
-      }
-    },
-    createMap() {
-      this.map = new AMap.Map('mapContainer', {
-        zoom: 16,
-        viewMode: '2D',
-        pitch: 45
-      })
-      this.map.add(new AMap.TileLayer.Satellite())
-      // ToolBar 在 v2 需要通过 plugin 异步加载;直接 new AMap.ToolBar() 会报:AMap.ToolBar is not a constructor
+  } catch (err) {
+    uni.showToast({ title: err.message, icon: 'none' })
+  } finally {
+    uni.hideLoading()
+  }
+}
+
+const initMap = () => {
+	// #ifdef H5
+  if (window.AMap) {
+    createMap()
+  } else {
+    const script = document.createElement('script')
+    script.src = `https://webapi.amap.com/maps?v=2.0&key=9f2cac7ea18905dd3830cf7360a43a35`
+    script.onload = createMap
+    document.head.appendChild(script)
+  }
+	// #endif
+	// #ifndef H5
+	// 非 H5 平台使用 uni-app 地图组件
+	console.warn('当前平台不支持动态加载高德地图脚本,请使用 uni-app 地图组件')
+	// #endif
+}
+
+const createMap = () => {
+  map.value = new AMap.Map('mapContainer', {
+    zoom: 16,
+    viewMode: '2D',
+    pitch: 45
+  })
+  map.value.add(new AMap.TileLayer.Satellite())
+  // ToolBar 在 v2 需要通过 plugin 异步加载;直接 new AMap.ToolBar() 会报:AMap.ToolBar is not a constructor
+  try {
+    AMap.plugin(['AMap.ToolBar'], () => {
       try {
-        AMap.plugin(['AMap.ToolBar'], () => {
-          try {
-            const toolBar = new AMap.ToolBar()
-            this.map.addControl(toolBar)
-          } catch (e) {
-            // eslint-disable-next-line no-console
-            console.warn('[job-detail] init toolbar failed', e)
-          }
-        })
+        const toolBar = new AMap.ToolBar()
+        map.value.addControl(toolBar)
       } catch (e) {
         // eslint-disable-next-line no-console
-        console.warn('[job-detail] AMap.plugin ToolBar failed', e)
+        console.warn('[job-detail] init toolbar failed', e)
       }
+    })
+  } catch (e) {
+    // eslint-disable-next-line no-console
+    console.warn('[job-detail] AMap.plugin ToolBar failed', e)
+  }
 
-      this.amapLoaded = true
-      // 地图准备好后,如果作业信息已加载,则渲染一次静态元素 & 启动轮询
-      if (this.jobInfo && Object.keys(this.jobInfo).length) {
-        this.updateMapElements()
-        this.setupPolling()
-      }
-    },
-    // 更新地图元素
-    updateMapElements() {
+  amapLoaded.value = true
+  // 地图准备好后,如果作业信息已加载,则渲染一次静态元素 & 启动轮询
+  if (jobInfo.value && Object.keys(jobInfo.value).length) {
+    updateMapElements()
+    setupPolling()
+  }
+}
+
+const updateMapElements = () => {
+  
+  // 你当前接口返回:jobInfo.workAreas.waypoints 直接就是点数组:[{lng,lat}, ...]
+  // 同时兼容未来可能的结构:
+  // - workAreas 是数组:[{ waypoints:[...] }, ...]
+  // - workAreas 是对象:{ waypoints:[...] }
+  if (!amapLoaded.value) return
+
+  const workAreas = jobInfo.value && jobInfo.value.workAreas
+  let points = []
+
+  // 情况A:workAreas.waypoints = [{lng,lat}, ...](你现在的情况)
+  // 注意:waypoints 有可能被后端返回成字符串 / 对象(单点)/ null,先做数组兜底
+  if (workAreas && workAreas.waypoints) {
+    try {
+      // 如果是字符串,尝试解析为 JSON
+      points = typeof workAreas.waypoints === 'string' 
+        ? JSON.parse(workAreas.waypoints)
+        : workAreas.waypoints;
       
-      // 你当前接口返回:jobInfo.workAreas.waypoints 直接就是点数组:[{lng,lat}, ...]
-      // 同时兼容未来可能的结构:
-      // - workAreas 是数组:[{ waypoints:[...] }, ...]
-      // - workAreas 是对象:{ waypoints:[...] }
-      if (!this.amapLoaded) return
-
-      const workAreas = this.jobInfo && this.jobInfo.workAreas
-      let points = []
-
-      // 情况A:workAreas.waypoints = [{lng,lat}, ...](你现在的情况)
-      // 注意:waypoints 有可能被后端返回成字符串 / 对象(单点)/ null,先做数组兜底
-      if (workAreas && workAreas.waypoints) {
-        try {
-          // 如果是字符串,尝试解析为 JSON
-          points = typeof workAreas.waypoints === 'string' 
-            ? JSON.parse(workAreas.waypoints)
-            : workAreas.waypoints;
-          
-          // 确保解析后是数组
-          if (!Array.isArray(points)) {
-            console.warn('[job-detail] workAreas.waypoints is not an array after parsing:', workAreas.waypoints);
-            points = [];
-          }
-        } catch (e) {
-          console.error('[job-detail] Failed to parse workAreas.waypoints:', e);
-          points = [];
-        }
+      // 确保解析后是数组
+      if (!Array.isArray(points)) {
+        console.warn('[job-detail] workAreas.waypoints is not an array after parsing:', workAreas.waypoints);
+        points = [];
+      }
+    } catch (e) {
+      console.error('[job-detail] Failed to parse workAreas.waypoints:', e);
+      points = [];
     }
+}
 
 
-      if (!points.length) return
-      console.log("points",points);
+  if (!points.length) return
+  console.log("points",points);
+
+  const waypoints = points
+    .filter(p => p && p.lng != null && p.lat != null)
+    .map(p => [p.lng, p.lat])
+
+    console.log("waypoints",waypoints);
+    
+  if (waypoints.length <= 2) return
+
+  // 1. 绘制作业区域
+  if (!areaPolygon.value) {
+    areaPolygon.value = new AMap.Polygon({
+      map: map.value,
+      path: waypoints,
+      strokeColor: '#28F',
+      strokeWeight: 2,
+      fillColor: '#28F',
+      fillOpacity: 0.1
+    })
+  } else {
+    areaPolygon.value.setPath(waypoints)
+  }
 
-      const waypoints = points
+  // 2. 绘制规划路线
+  // 注意:AMap.Polyline 的 path 需要是 [[lng,lat], ...] 或 AMap.LngLat[]。
+  // 这里统一把 routePoints 归一化成 [[lng,lat], ...],避免出现 undefined[0] 这类错误。
+  const rawRoutePoints = jobInfo.value.routePoints || []
+
+  let routePath = []
+  if (Array.isArray(rawRoutePoints) && rawRoutePoints.length) {
+    // 可能是 [{lng,lat},...] 或 [[lng,lat],...]
+    const first = rawRoutePoints[0]
+    if (Array.isArray(first)) {
+      routePath = rawRoutePoints
+    } else {
+      routePath = rawRoutePoints
         .filter(p => p && p.lng != null && p.lat != null)
         .map(p => [p.lng, p.lat])
+    }
+  } else {
+    // 没有 routePoints 就用区域边界 waypoints(它已经是 [[lng,lat],...])
+    routePath = waypoints
+  }
 
-        console.log("waypoints",waypoints);
-        
-      if (waypoints.length <= 2) return
-
-      // 1. 绘制作业区域
-      if (!this.areaPolygon) {
-        this.areaPolygon = new AMap.Polygon({
-          map: this.map,
-          path: waypoints,
-          strokeColor: '#28F',
-          strokeWeight: 2,
-          fillColor: '#28F',
-          fillOpacity: 0.1
-        })
-      } else {
-        this.areaPolygon.setPath(waypoints)
-      }
+  if (routePath.length >= 2) {
+    if (!routePolyline.value) {
+      routePolyline.value = new AMap.Polyline({
+        map: map.value,
+        path: routePath,
+        showDir: true,
+        strokeColor: '#28F',
+        strokeWeight: 6
+      })
+    } else {
+      routePolyline.value.setPath(routePath)
+    }
+  }
 
-      // 2. 绘制规划路线
-      // 注意:AMap.Polyline 的 path 需要是 [[lng,lat], ...] 或 AMap.LngLat[]。
-      // 这里统一把 routePoints 归一化成 [[lng,lat], ...],避免出现 undefined[0] 这类错误。
-      const rawRoutePoints = this.jobInfo.routePoints || []
-
-      let routePath = []
-      if (Array.isArray(rawRoutePoints) && rawRoutePoints.length) {
-        // 可能是 [{lng,lat},...] 或 [[lng,lat],...]
-        const first = rawRoutePoints[0]
-        if (Array.isArray(first)) {
-          routePath = rawRoutePoints
-        } else {
-          routePath = rawRoutePoints
-            .filter(p => p && p.lng != null && p.lat != null)
-            .map(p => [p.lng, p.lat])
-        }
-      } else {
-        // 没有 routePoints 就用区域边界 waypoints(它已经是 [[lng,lat],...])
-        routePath = waypoints
-      }
+  // 3. 绘制已走轨迹 (假设后端返回 passedPoints)
+  const rawPassedPoints = jobInfo.value.passedPoints || []
 
-      if (routePath.length >= 2) {
-        if (!this.routePolyline) {
-          this.routePolyline = new AMap.Polyline({
-            map: this.map,
-            path: routePath,
-            showDir: true,
-            strokeColor: '#28F',
-            strokeWeight: 6
-          })
-        } else {
-          this.routePolyline.setPath(routePath)
-        }
-      }
+  let passedPath = []
+  if (Array.isArray(rawPassedPoints) && rawPassedPoints.length) {
+    const first = rawPassedPoints[0]
+    if (Array.isArray(first)) {
+      passedPath = rawPassedPoints
+    } else {
+      passedPath = rawPassedPoints
+        .filter(p => p && p.lng != null && p.lat != null)
+        .map(p => [p.lng, p.lat])
+    }
+  }
 
-      // 3. 绘制已走轨迹 (假设后端返回 passedPoints)
-      const rawPassedPoints = this.jobInfo.passedPoints || []
-
-      let passedPath = []
-      if (Array.isArray(rawPassedPoints) && rawPassedPoints.length) {
-        const first = rawPassedPoints[0]
-        if (Array.isArray(first)) {
-          passedPath = rawPassedPoints
-        } else {
-          passedPath = rawPassedPoints
-            .filter(p => p && p.lng != null && p.lat != null)
-            .map(p => [p.lng, p.lat])
-        }
-      }
+  // 3. 绘制已走轨迹:优先用实时轮询轨迹;否则使用后端历史 passedPoints
+  const finalPassedPath = (realtimeTrackPoints.value && realtimeTrackPoints.value.length)
+    ? realtimeTrackPoints.value
+    : passedPath
+
+  if (!passedPolyline.value) {
+    passedPolyline.value = new AMap.Polyline({
+      map: map.value,
+      strokeColor: '#AF5',
+      strokeWeight: 6
+    })
+  }
+  passedPolyline.value.setPath(finalPassedPath)
+
+  // 4. 绘制设备位置
+  const devicePosition = jobInfo.value.currentPosition
+  
+  if (devicePosition && devicePosition.lng) {
+    if (!deviceMarker.value) {
+      deviceMarker.value = new AMap.Marker({ map: map.value, position: [devicePosition.lng, devicePosition.lat]})
+    } else {
+      deviceMarker.value.setPosition([devicePosition.lng, devicePosition.lat])
+    }
+    map.value.setCenter([devicePosition.lng, devicePosition.lat])
+  }
 
-      // 3. 绘制已走轨迹:优先用实时轮询轨迹;否则使用后端历史 passedPoints
-      const finalPassedPath = (this.realtimeTrackPoints && this.realtimeTrackPoints.length)
-        ? this.realtimeTrackPoints
-        : passedPath
-
-      if (!this.passedPolyline) {
-        this.passedPolyline = new AMap.Polyline({
-          map: this.map,
-          strokeColor: '#AF5',
-          strokeWeight: 6
-        })
-      }
-      this.passedPolyline.setPath(finalPassedPath)
+  // 首次渲染静态元素时再 fitView,实时更新时不要频繁 fitView
+  map.value.setFitView()
+}
 
-      // 4. 绘制设备位置
-      const devicePosition = this.jobInfo.currentPosition
-      
-      if (devicePosition && devicePosition.lng) {
-        if (!this.deviceMarker) {
-          this.deviceMarker = new AMap.Marker({ map: this.map, position: [devicePosition.lng, devicePosition.lat]})
-        } else {
-          this.deviceMarker.setPosition([devicePosition.lng, devicePosition.lat])
-        }
-        this.map.setCenter([devicePosition.lng, devicePosition.lat])
-      }
+const setupPolling = () => {
+  // 先清一次,避免重复创建 interval
+  clearPolling()
 
-      // 首次渲染静态元素时再 fitView,实时更新时不要频繁 fitView
-      this.map.setFitView()
-    },
-    // 设置轮询(实时位置 + 轨迹)
-    setupPolling() {
-      // 先清一次,避免重复创建 interval
-      this.clearPolling()
-
-      // 没有 deviceId 就无法拉实时数据
-      const deviceId = this.deviceId
-      if (!deviceId) return
-
-      // 仅在进行中/暂停时轮询(你也可以放开为所有状态)
-      if (![1, 3].includes(this.jobStatus)) return
-
-      // 立刻拉一次,避免要等第一轮 interval
-      this.fetchRealtimeAndUpdate()
-
-      // 3 秒轮询一次
-      this.pollingTimer = setInterval(() => {
-        this.fetchRealtimeAndUpdate()
-      }, 3000)
-    },
-
-    clearPolling() {
-      if (this.pollingTimer) {
-        clearInterval(this.pollingTimer)
-        this.pollingTimer = null
-      }
-    },
+  // 没有 deviceId 就无法拉实时数据
+  const deviceIdValue = deviceId.value
+  if (!deviceIdValue) return
 
-    async fetchRealtimeAndUpdate() {
-      try {
-        console.log("this.jobInfo",this.jobInfo);
-        
-        const deviceId =this.deviceId
-        if (!deviceId) return
-
-        const res = await getRealtimeData(deviceId)
-        const payload = res && res.data && (res.data.data || res.data)
-
-        if (!payload) return
-
-        // 检查作业状态,如果已完成则提示并退出
-        if (payload.state === 3 || payload.state === 'FINISHED' ) {
-          this.clearPolling()
-          uni.showModal({
-            title: '提示',
-            content: '当前作业已完成,是否退出当前页面',
-            success: (modalRes) => {
-              if (modalRes.confirm) {
-                uni.navigateBack()
-              }
-            }
-          })
-          return
-        }
+  // 仅在进行中/暂停时轮询(你也可以放开为所有状态)
+  if (![1, 3].includes(jobStatus.value)) return
 
-        // 可选:丢弃时间倒退的数据
-        const reportTime = payload.reportTime
-        if (reportTime && this.lastReportTime && reportTime < this.lastReportTime) {
-          return
-        }
-        if (reportTime) this.lastReportTime = reportTime
+  // 立刻拉一次,避免要等第一轮 interval
+  fetchRealtimeAndUpdate()
 
-        // 更新头部信息(如果接口有)
-        if (payload.progress != null) this.jobProgress = payload.progress
-        if (payload.speed != null) this.deviceSpeed = payload.speed
-        if (payload.battery != null) this.batteryLevel = payload.battery
+  // 3 秒轮询一次
+  pollingTimer.value = setInterval(() => {
+    fetchRealtimeAndUpdate()
+  }, 3000)
+}
 
-        const pt = payload.currentPoint
-        if (!pt || pt.x == null || pt.y == null) return
+const clearPolling = () => {
+  if (pollingTimer.value) {
+    clearInterval(pollingTimer.value)
+    pollingTimer.value = null
+  }
+}
 
-        // currentPoint: {x,y} 这里按接口文档理解为 (lng,lat)
-        const lngLat = [pt.x, pt.y]
-        
-        // 获取行驶方向
-        const direction = payload.direction || 0
+const fetchRealtimeAndUpdate = async () => {
+  try {
+    console.log("jobInfo.value",jobInfo.value);
+    
+    const deviceIdValue = deviceId.value
+    if (!deviceIdValue) return
+
+    const res = await getRealtimeData(deviceIdValue)
+    const payload = res && res.data && (res.data.data || res.data)
+
+    if (!payload) return
+
+    // 检查作业状态,如果已完成则提示并退出
+    if (payload.state === 3 || payload.state === 'FINISHED' ) {
+      clearPolling()
+      uni.showModal({
+        title: '提示',
+        content: '当前作业已完成,是否退出当前页面',
+        success: (modalRes) => {
+          if (modalRes.confirm) {
+            uni.navigateBack()
+          }
+        }
+      })
+      return
+    }
 
-        this.updateDeviceMarker(lngLat, direction)
-        this.appendTrackPoint(lngLat)
-        this.updatePassedPolyline()
-      } catch (e) {
-        // eslint-disable-next-line no-console
-        console.warn('[job-detail] fetchRealtimeAndUpdate failed', e)
-      }
-    },
+    // 可选:丢弃时间倒退的数据
+    const reportTime = payload.reportTime
+    if (reportTime && lastReportTime.value && reportTime < lastReportTime.value) {
+      return
+    }
+    if (reportTime) lastReportTime.value = reportTime
+
+    // 更新头部信息(如果接口有)
+    if (payload.progress != null) jobProgress.value = payload.progress
+    if (payload.speed != null) deviceSpeed.value = payload.speed
+    if (payload.battery != null) batteryLevel.value = payload.battery
+
+    const pt = payload.currentPoint
+    if (!pt || pt.x == null || pt.y == null) return
+
+    // currentPoint: {x,y} 这里按接口文档理解为 (lng,lat)
+    const lngLat = [pt.x, pt.y]
+    
+    // 获取行驶方向
+    const direction = payload.direction || 0
+
+    updateDeviceMarker(lngLat, direction)
+    appendTrackPoint(lngLat)
+    updatePassedPolyline()
+  } catch (e) {
+    // eslint-disable-next-line no-console
+    console.warn('[job-detail] fetchRealtimeAndUpdate failed', e)
+  }
+}
 
-    updateDeviceMarker(lngLat, direction = 0) {
-      if (!this.amapLoaded || !this.map) return
-      if (!lngLat || lngLat.length !== 2) return
-      
-      // 创建一个 Icon
-      var startIcon = new AMap.Icon({
-          // 图标尺寸
-          size: new AMap.Size(45, 45),
-          // 图标的取图地址
-          image: '/static/icons/gecaoji.png',
-          // 图标所用图片大小
-          imageSize: new AMap.Size(45, 45),
-          // 图标取图偏移量
-          // imageOffset: new AMap.Pixel(-9, -3)
-      });
-      
-      if (!this.deviceMarker) {
-        this.deviceMarker = new AMap.Marker({
-          map: this.map,
-          position: lngLat,
-          icon: startIcon,
-          anchor: 'center', // 设置锚点为中心,使图标居中显示在路线上
-          angle: direction // 设置初始旋转角度
-        })
-      } else {
-        this.deviceMarker.setPosition(lngLat)
-        this.deviceMarker.setAngle(direction) // 更新旋转角度
-      }
+const updateDeviceMarker = (lngLat, direction = 0) => {
+  if (!amapLoaded.value || !map.value) return
+  if (!lngLat || lngLat.length !== 2) return
+  
+  // 创建一个 Icon
+  var startIcon = new AMap.Icon({
+      // 图标尺寸
+      size: new AMap.Size(45, 45),
+      // 图标的取图地址
+      image: '/static/icons/gecaoji.png',
+      // 图标所用图片大小
+      imageSize: new AMap.Size(45, 45),
+      // 图标取图偏移量
+      // imageOffset: new AMap.Pixel(-9, -3)
+  });
+  
+  if (!deviceMarker.value) {
+    deviceMarker.value = new AMap.Marker({
+      map: map.value,
+      position: lngLat,
+      icon: startIcon,
+      anchor: 'center', // 设置锚点为中心,使图标居中显示在路线上
+      angle: direction // 设置初始旋转角度
+    })
+  } else {
+    deviceMarker.value.setPosition(lngLat)
+    deviceMarker.value.setAngle(direction) // 更新旋转角度
+  }
 
-      // 轻量跟随:中心跟随但不 fitView,避免每次抖动缩放
-      this.map.setCenter(lngLat)
-    },
+  // 轻量跟随:中心跟随但不 fitView,避免每次抖动缩放
+  map.value.setCenter(lngLat)
+}
 
-    appendTrackPoint(lngLat) {
-      if (!lngLat || lngLat.length !== 2) return
+const appendTrackPoint = (lngLat) => {
+  if (!lngLat || lngLat.length !== 2) return
 
-      const last = this.realtimeTrackPoints.length
-        ? this.realtimeTrackPoints[this.realtimeTrackPoints.length - 1]
-        : null
+  const last = realtimeTrackPoints.value.length
+    ? realtimeTrackPoints.value[realtimeTrackPoints.value.length - 1]
+    : null
 
-      // 去重:相同点不追加
-      if (last && last[0] === lngLat[0] && last[1] === lngLat[1]) return
+  // 去重:相同点不追加
+  if (last && last[0] === lngLat[0] && last[1] === lngLat[1]) return
 
-      this.realtimeTrackPoints.push(lngLat)
-    },
+  realtimeTrackPoints.value.push(lngLat)
+}
 
-    updatePassedPolyline() {
-      if (!this.amapLoaded || !this.map) return
+const updatePassedPolyline = () => {
+  if (!amapLoaded.value || !map.value) return
+
+  // 至少两个点才画线
+  if (!realtimeTrackPoints.value || realtimeTrackPoints.value.length < 2) return
+
+  if (!passedPolyline.value) {
+    passedPolyline.value = new AMap.Polyline({
+      map: map.value,
+      path: realtimeTrackPoints.value,
+      strokeColor: '#AF5',
+      strokeWeight: 6
+    })
+  } else {
+    passedPolyline.value.setPath(realtimeTrackPoints.value)
+  }
+}
 
-      // 至少两个点才画线
-      if (!this.realtimeTrackPoints || this.realtimeTrackPoints.length < 2) return
+const handleAction = async (action) => {
+  const actions = {
+    pause: { api: pauseTask, msg: '作业已暂停' },
+    resume: { api: startTask, msg: '作业已继续' },
+    stop: { api: stopTask, msg: '作业已停止' },
+    recall: { api: recallTask, msg: '召回指令已发送' }
+  }
+  const currentAction = actions[action]
+
+  if (action === 'stop') {
+    uni.showModal({
+      title: '确认停止',
+      content: '停止后无法恢复,确定吗?',
+      success: res => res.confirm && executeAction(currentAction)
+    })
+  } else {
+    executeAction(currentAction)
+  }
+}
 
-      if (!this.passedPolyline) {
-        this.passedPolyline = new AMap.Polyline({
-          map: this.map,
-          path: this.realtimeTrackPoints,
-          strokeColor: '#AF5',
-          strokeWeight: 6
-        })
-      } else {
-        this.passedPolyline.setPath(this.realtimeTrackPoints)
-      }
-    },
-    // 处理按钮操作
-    async handleAction(action) {
-      const actions = {
-        pause: { api: pauseTask, msg: '作业已暂停' },
-        resume: { api: startTask, msg: '作业已继续' },
-        stop: { api: stopTask, msg: '作业已停止' },
-        recall: { api: recallTask, msg: '召回指令已发送' }
-      }
-      const currentAction = actions[action]
-
-      if (action === 'stop') {
-        uni.showModal({
-          title: '确认停止',
-          content: '停止后无法恢复,确定吗?',
-          success: res => res.confirm && this.executeAction(currentAction)
-        })
-      } else {
-        this.executeAction(currentAction)
-      }
-    },
-    async executeAction({ api, msg }) {
-      try {
-        const res = await api(this.jobId)
-        if (res.data.code === 200) {
-          uni.showToast({ title: msg, icon: 'success' })
-          this.loadJobDetails()
-        } else {
-          throw new Error(res.data.msg || '操作失败')
-        }
-      } catch (err) {
-        uni.showToast({ title: err.message, icon: 'none' })
-      }
-    },
-    // 格式化时间
-    formatTime(timeStr) {
-      if (!timeStr) return '--'
-      return timeStr.substring(0, 16)
+const executeAction = async ({ api, msg }) => {
+  try {
+    const res = await api(jobId.value)
+    if (res.data.code === 200) {
+      uni.showToast({ title: msg, icon: 'success' })
+      loadJobDetails()
+    } else {
+      throw new Error(res.data.msg || '操作失败')
     }
+  } catch (err) {
+    uni.showToast({ title: err.message, icon: 'none' })
   }
 }
+
+const formatTime = (timeStr) => {
+  if (!timeStr) return '--'
+  return timeStr.substring(0, 16)
+}
 </script>
 
 <style scoped>
@@ -680,4 +675,3 @@ export default {
   background-color: #52c41a;
 }
 </style>
-

+ 713 - 648
pages/knowledge/ai-chat/index.vue

@@ -2,13 +2,9 @@
 	<view class="container">
 		<!-- 聊天记录区域 -->
 		<scroll-view class="chat-container" scroll-y :scroll-top="scrollTop" :scroll-with-animation="true"
-			@scroll="onScroll" :style="{ 
-        height: `calc(100vh - ${inputHeight}px)`,
-        marginTop: '0'
-      }">
+			@scroll="onScroll" :style="{ height: `calc(100vh - ${inputHeight}px)`,marginTop: '0'}">
 			<view class="chat-list">
-				<view v-for="(message, index) in chatMessages" :key="index" class="message-item"
-					:class="{ 'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user' }">
+				<view v-for="(message, index) in chatMessages" :key="index" class="message-item" :class="{ 'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user' }">
 					<!-- AI消息 -->
 					<template v-if="message.sender === 'ai'">
 						<view class="avatar-container">
@@ -58,7 +54,6 @@
 				</view>
 			</view>
 		</scroll-view>
-
 		<!-- 底部输入区 -->
 		<view class="input-container" :style="{ paddingBottom: `${isIOS ? safeAreaBottom : 20}rpx` }">
 			<!-- 问题建议区 -->
@@ -93,694 +88,764 @@
 		<view 
 			:change:prop="renderModule.onDataChange" 
 			:prop="renderjsData"
+			:onStreamData="onStreamData"
 			class="renderjs-container"
 		></view>
 	</view>
 </template>
-
-<script>
+<script setup>
+	import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick, getCurrentInstance } from 'vue'
 	import api from "@/config/api.js";
 	import storage from "@/utils/storage.js";
 	import { chatStreamSuggested } from "@/api/services/chart.js";
-	export default {
-		data() {
-			return {
-				inputMessage: '', // 输入框消息
-				chatMessages: [{
-					sender: 'ai',
-					content: '您好!我是农小禹,您的智能农业助手🌱 我可以帮您解答农业种植、病虫害防治、农产品管理等方面的问题。有什么可以帮助您的吗?',
-					time: this.getFormattedTime(new Date()),
-					timestamp: Date.now(),
-					isWelcome: true
-				}],
-				scrollTop: 0,
-				inputHeight: 110,
-				isProcessing: false,
-				currentTypingMessage: null, // 当前正在输入的消息索引
-				lastUserQuestion: '', // 保存最后一个用户问题,用于重新生成
-				suggestedQuestions: [
-					'水稻插秧后如何管理?',
-					'果树夏季修剪技巧?',
-					'如何防治蔬菜常见病虫害?',
-					'农药使用注意事项?',
-					'有机肥和化肥怎么搭配使用?'
-				],
-				statusBarHeight: 20,
-				safeAreaBottom: 34,
-				isIOS: false,
-				// renderjs 通信数据
-				renderjsData: {
-					action: '', // start, stop
-					url: '',
-					data: {},
-					timestamp: 0
-				},
-				// 消息队列和打字机效果
-				messageQueue: [],
-				isTypingEffect: false,
-				typingTimer: null,
-				// Thinking 模式追踪
-				isInThinkingMode: false,
-				sessionId: null,
-				messageId: null,// 消息ID,用于标识当前消息
-				thinkingBuffer: '' // 临时存储 Thinking 内容
+	
+	// 响应式数据
+	const inputMessage = ref('') // 输入框消息
+	
+	// 初始化函数,用于获取初始时间
+	const getInitialFormattedTime = () => {
+		const date = new Date()
+		const hours = date.getHours().toString().padStart(2, '0')
+		const minutes = date.getMinutes().toString().padStart(2, '0')
+		return `${hours}:${minutes}`
+	}
+	
+	const chatMessages = ref([{
+		sender: 'ai',
+		content: '您好!我是农小禹,您的智能农业助手🌱 我可以帮您解答农业种植、病虫害防治、农产品管理等方面的问题。有什么可以帮助您的吗?',
+		time: getInitialFormattedTime(),
+		timestamp: Date.now(),
+		isWelcome: true
+	}])
+	
+	const scrollTop = ref(0)
+	const inputHeight = ref(110)
+	const isProcessing = ref(false)
+	const currentTypingMessage = ref(null) // 当前正在输入的消息索引
+	const lastUserQuestion = ref('') // 保存最后一个用户问题,用于重新生成
+	const suggestedQuestions = ref([
+		'水稻插秧后如何管理?',
+		'果树夏季修剪技巧?',
+		'如何防治蔬菜常见病虫害?',
+		'农药使用注意事项?',
+		'有机肥和化肥怎么搭配使用?'
+	])
+	const statusBarHeight = ref(20)
+	const safeAreaBottom = ref(34)
+	const isIOS = ref(false)
+	
+	// renderjs 通信数据
+	const renderjsData = ref({
+		action: '', // start, stop
+		url: '',
+		data: {},
+		timestamp: 0
+	})
+	
+	// 消息队列和打字机效果
+	const messageQueue = ref([])
+	const isTypingEffect = ref(false)
+	const typingTimer = ref(null)
+	
+	// Thinking 模式追踪
+	const isInThinkingMode = ref(false)
+	const sessionId = ref(null)
+	const messageId = ref(null) // 消息ID,用于标识当前消息
+	const thinkingBuffer = ref('') // 临时存储 Thinking 内容
+	
+	// 防抖函数引用
+	let debouncedSubmitQuestion = null
+	
+	// uni-app 生命周期
+	const onNavigationBarButtonTap = (e) => {
+		console.log("导航栏按钮点击:", e);
+	}
+	
+	// ========== renderjs 通信方法 ==========
+	
+	// renderjs 回调:接收流式数据
+	const onStreamData = (data) => {
+		console.log('=== Vue收到流式数据 ===', data);
+		console.log('数据类型:', data.type);
+		console.log('当前输入消息索引:', currentTypingMessage.value);
+		
+		if (data.type === 'thinking') {
+			console.log('进入 Thinking 模式');
+			// 第一个 thinking 类型,开启 Thinking 模式
+			isInThinkingMode.value = true;
+			thinkingBuffer.value = data.content;
+			processThinkingContent();
+		} else if (data.type === 'message') {
+			console.log('收到消息内容:', data.content);
+			// 判断是否在 Thinking 模式中
+			if (isInThinkingMode.value) {
+				console.log('仍在 Thinking 模式,累积内容');
+				// 仍在 Thinking 区域内,累积内容
+				thinkingBuffer.value += data.content;
+				processThinkingContent();
+			} else {
+				console.log('普通消息,添加到队列');
+				// 普通消息内容
+				handleMessageData(data.content);
 			}
-		},
-		// 设置页面标题
-		onNavigationBarButtonTap(e) {
-			console.log("导航栏按钮点击:", e);
-		},
-		created() {
-			this.debouncedSubmitQuestion = this.debounce(this.submitQuestion, 300)
-		},
-		mounted() {
-			// 获取系统信息
-			const systemInfo = uni.getSystemInfoSync();
-			this.statusBarHeight = systemInfo.statusBarHeight || 20;
-			this.isIOS = systemInfo.platform === 'ios';
-			this.safeAreaBottom = systemInfo.safeAreaInsets ? (systemInfo.safeAreaInsets.bottom || 0) : 0;
-
-			// 初始化消息
-			this.initMessages();
-
-			// 滚动到底部
-			this.$nextTick(() => {
-				this.scrollToBottom();
-			});
-		},
-		methods: {
-			// ========== renderjs 通信方法 ==========
-			
-			// renderjs 回调:接收流式数据
-			onStreamData(data) {
-				console.log('收到流式数据:', data);
-				
-				if (data.type === 'thinking') {
-					// 第一个 thinking 类型,开启 Thinking 模式
-					this.isInThinkingMode = true;
-					this.thinkingBuffer = data.content;
-					this.processThinkingContent();
-				} else if (data.type === 'message') {
-					// 判断是否在 Thinking 模式中
-					if (this.isInThinkingMode) {
-						// 仍在 Thinking 区域内,累积内容
-						this.thinkingBuffer += data.content;
-						this.processThinkingContent();
-					} else {
-						// 普通消息内容
-						this.handleMessageData(data.content);
-					}
-				} else if (data.type === 'end') {
-					// 流式结束
-					console.log("消息id:",data.id);
-					
-					this.finishStreaming(data.id);
-				} else if (data.type === 'error') {
-					// 错误处理
-					this.handleStreamError(data.error);
-				}
-			},
+		} else if (data.type === 'end') {
+			// 流式结束
+			console.log("流式结束,消息id:",data.id);
 			
-			// 处理 Thinking 内容(跳过 Thinking,只处理后续内容)
-			processThinkingContent() {
-				if (this.currentTypingMessage === null) return;
-				
-				// 检查是否包含 </details> 结束标签
-				if (this.thinkingBuffer.includes('</details>')) {
-					// Thinking 区域结束,提取 </details> 之后的内容
-					this.isInThinkingMode = false;
-					
-					// 提取 </details> 后面的内容
-					const detailsEndIndex = this.thinkingBuffer.indexOf('</details>');
-					const contentAfterThinking = this.thinkingBuffer.substring(detailsEndIndex + '</details>'.length);
-					
-					// 如果有内容,添加到消息队列
-					if (contentAfterThinking) {
-						this.handleMessageData(contentAfterThinking);
-					}
-					
-					// 清空缓冲区
-					this.thinkingBuffer = '';
-				}
-				// 如果还在 Thinking 区域内,不做任何显示,继续累积
-			},
-			
-			// 处理消息数据(添加到队列)
-			handleMessageData(text) {
-				if (!text) return;
-				
-				// 将文本按字符添加到队列
-				for (let char of text) {
-					this.messageQueue.push(char);
-				}
-				
-				// 如果打字机效果未启动,则启动
-				if (!this.isTypingEffect) {
-					this.startTypingEffect();
-				}
-			},
+			finishStreaming(data.id);
+		} else if (data.type === 'error') {
+			// 错误处理
+			console.error('流式错误:', data.error);
+			handleStreamError(data.error);
+		}
+	}
+	
+	// 处理 Thinking 内容(跳过 Thinking,只处理后续内容)
+	const processThinkingContent = () => {
+		if (currentTypingMessage.value === null) return;
+		
+		// 检查是否包含 </details> 结束标签
+		if (thinkingBuffer.value.includes('</details>')) {
+			// Thinking 区域结束,提取 </details> 之后的内容
+			isInThinkingMode.value = false;
 			
-			// 启动打字机效果
-			startTypingEffect() {
-				if (this.isTypingEffect || this.currentTypingMessage === null) return;
-				
-				this.isTypingEffect = true;
-				this.$set(this.chatMessages[this.currentTypingMessage], 'isTyping', false);
-				
-				const processQueue = () => {
-					if (this.messageQueue.length > 0 && this.currentTypingMessage !== null) {
-						// 每次取出一个字符
-						const char = this.messageQueue.shift();
-						const currentContent = this.chatMessages[this.currentTypingMessage].content || '';
-						this.$set(this.chatMessages[this.currentTypingMessage], 'content', currentContent + char);
-						
-						// 每20个字符滚动一次,优化性能
-						if (currentContent.length % 20 === 0) {
-							this.scrollToBottom();
-						}
-						
-						// 继续处理队列
-						this.typingTimer = setTimeout(processQueue, 10);
-					} else if (this.messageQueue.length === 0) {
-						// 队列为空,等待新数据
-						this.typingTimer = setTimeout(processQueue, 50);
-					}
-				};
-				
-				processQueue();
-			},
+			// 提取 </details> 后面的内容
+			const detailsEndIndex = thinkingBuffer.value.indexOf('</details>');
+			const contentAfterThinking = thinkingBuffer.value.substring(detailsEndIndex + '</details>'.length);
 			
-			// 停止打字机效果
-			stopTypingEffect() {
-				this.isTypingEffect = false;
-				if (this.typingTimer) {
-					clearTimeout(this.typingTimer);
-					this.typingTimer = null;
-				}
-				
-				// 清空队列,将剩余内容一次性显示
-				if (this.messageQueue.length > 0 && this.currentTypingMessage !== null) {
-					const remainingText = this.messageQueue.join('');
-					const currentContent = this.chatMessages[this.currentTypingMessage].content || '';
-					this.$set(this.chatMessages[this.currentTypingMessage], 'content', currentContent + remainingText);
-					this.messageQueue = [];
-				}
-			},
+			// 如果有内容,添加到消息队列
+			if (contentAfterThinking) {
+				handleMessageData(contentAfterThinking);
+			}
 			
-			// 完成流式输出
-			finishStreaming(id) {
-				console.log("消息id2:",id);
-				this.stopTypingEffect();
+			// 清空缓冲区
+			thinkingBuffer.value = '';
+		}
+		// 如果还在 Thinking 区域内,不做任何显示,继续累积
+	}
+	
+	// 处理消息数据(添加到队列)
+	const handleMessageData = (text) => {
+		console.log('handleMessageData 被调用,文本长度:', text ? text.length : 0);
+		if (!text) return;
+		
+		// 将文本按字符添加到队列
+		for (let char of text) {
+			messageQueue.value.push(char);
+		}
+		console.log('消息队列长度:', messageQueue.value.length);
+		
+		// 如果打字机效果未启动,则启动
+		if (!isTypingEffect.value) {
+			console.log('启动打字机效果');
+			startTypingEffect();
+		}
+	}
+	
+	// 启动打字机效果
+	const startTypingEffect = () => {
+		console.log('startTypingEffect 被调用');
+		console.log('isTypingEffect:', isTypingEffect.value);
+		console.log('currentTypingMessage:', currentTypingMessage.value);
+		
+		if (isTypingEffect.value || currentTypingMessage.value === null) {
+			console.log('打字机效果已在运行或没有当前消息,退出');
+			return;
+		}
+		
+		isTypingEffect.value = true;
+		chatMessages.value[currentTypingMessage.value].isTyping = false;
+		console.log('开始打字机效果,消息索引:', currentTypingMessage.value);
+		
+		const processQueue = () => {
+			if (messageQueue.value.length > 0 && currentTypingMessage.value !== null) {
+				// 每次取出一个字符
+				const char = messageQueue.value.shift();
+				const currentContent = chatMessages.value[currentTypingMessage.value].content || '';
+				chatMessages.value[currentTypingMessage.value].content = currentContent + char;
 				
-				if (this.currentTypingMessage !== null) {
-					this.$set(this.chatMessages[this.currentTypingMessage], 'isTyping', false);
-					this.currentTypingMessage = null;
+				// 每20个字符滚动一次,优化性能
+				if (currentContent.length % 20 === 0) {
+					scrollToBottom();
 				}
 				
-				// 重置 Thinking 模式状态
-				this.isInThinkingMode = false;
-				this.thinkingBuffer = '';
-				
-				this.isProcessing = false;
-				this.scrollToBottom();
-				
-				// 获取下一轮建议问题
-				// this.fetchSuggestedQuestions({user: this.sessionId,messageId:id});
-			},
-			
-			// 处理流式错误
-			handleStreamError(error) {
-				console.error('流式错误:', error);
-				this.stopTypingEffect();
-				
-				// 移除正在输入的消息
-				if (this.currentTypingMessage !== null) {
-					this.chatMessages.splice(this.currentTypingMessage, 1);
-					this.currentTypingMessage = null;
+				// 继续处理队列
+				typingTimer.value = setTimeout(processQueue, 10);
+			} else if (messageQueue.value.length === 0) {
+				// 队列为空,等待新数据
+				typingTimer.value = setTimeout(processQueue, 50);
+			}
+		};
+		
+		processQueue();
+	}
+	
+	// 停止打字机效果
+	const stopTypingEffect = () => {
+		isTypingEffect.value = false;
+		if (typingTimer.value) {
+			clearTimeout(typingTimer.value);
+			typingTimer.value = null;
+		}
+		
+		// 清空队列,将剩余内容一次性显示
+		if (messageQueue.value.length > 0 && currentTypingMessage.value !== null) {
+			const remainingText = messageQueue.value.join('');
+			const currentContent = chatMessages.value[currentTypingMessage.value].content || '';
+			chatMessages.value[currentTypingMessage.value].content = currentContent + remainingText;
+			messageQueue.value = [];
+		}
+	}
+	
+	// 完成流式输出
+	const finishStreaming = (id) => {
+		console.log("消息id2:",id);
+		stopTypingEffect();
+		
+		if (currentTypingMessage.value !== null) {
+			chatMessages.value[currentTypingMessage.value].isTyping = false;
+			currentTypingMessage.value = null;
+		}
+		
+		// 重置 Thinking 模式状态
+		isInThinkingMode.value = false;
+		thinkingBuffer.value = '';
+		
+		isProcessing.value = false;
+		scrollToBottom();
+		
+		// 获取下一轮建议问题
+		// fetchSuggestedQuestions({user: sessionId.value, messageId: id});
+	}
+	
+	// 处理流式错误
+	const handleStreamError = (error) => {
+		console.error('流式错误:', error);
+		stopTypingEffect();
+		
+		// 移除正在输入的消息
+		if (currentTypingMessage.value !== null) {
+			chatMessages.value.splice(currentTypingMessage.value, 1);
+			currentTypingMessage.value = null;
+		}
+		
+		// 重置 Thinking 模式状态
+		isInThinkingMode.value = false;
+		thinkingBuffer.value = '';
+		
+		isProcessing.value = false;
+		handleError({ message: error || '网络异常,请稍后重试' });
+	}
+
+	// ========== 用户交互方法 ==========
+	
+	// 发送消息
+	const submitQuestion = () => {
+		if (!storage.getHasLogin()) {
+			uni.showModal({
+				title: '提示',
+				content: '您还未登录,请先登录',
+				confirmText: '去登录',
+				cancelText: '取消',
+				success: function(res) {
+					if (res.confirm) {
+						uni.navigateTo({
+							url: '/pages/login/index'
+						});
+					}
+				},
+			});
+			return;
+		}
+		
+		if (!inputMessage.value.trim() || isProcessing.value) return;
+
+		const question = inputMessage.value.trim();
+		lastUserQuestion.value = question; // 保存问题用于重新生成
+		inputMessage.value = '';
+		
+		// 添加用户消息
+		chatMessages.value.push({
+			sender: 'user',
+			content: question,
+			time: getCurrentTime(),
+			timestamp: Date.now()
+		});
+
+		// 添加 AI 正在输入的消息
+		const typingMessageIndex = chatMessages.value.push({
+			sender: 'ai',
+			content: '',
+			time: getCurrentTime(),
+			timestamp: Date.now(),
+			isTyping: true
+		}) - 1;
+
+		currentTypingMessage.value = typingMessageIndex;
+		isProcessing.value = true;
+		scrollToBottom();
+		
+		// 通过 renderjs 发起 SSE 请求
+		startSSERequest(question);
+	}
+	
+	// 启动 SSE 请求(通过 renderjs)
+	const startSSERequest = (question) => {
+		const url = api.serve + '/uniapp/dify/chat/stream';
+		sessionId.value = Date.now().toString()
+		const requestData = {
+			query: question,
+			user: 'user_' + sessionId.value
+		};
+		
+		// 更新 renderjs 数据,触发 SSE 连接
+		renderjsData.value = {
+			action: 'start',
+			url: url,
+			data: requestData,
+			token: storage.getAccessToken(),
+			timestamp: Date.now()
+		};
+	}
+	
+	// 停止流式输出
+	const stopStreaming = () => {
+		console.log('用户中止流式输出');
+		
+		// 通知 renderjs 停止
+		renderjsData.value = {
+			action: 'stop',
+			timestamp: Date.now()
+		};
+		
+		// 立即停止打字机效果并完成
+		finishStreaming();
+		
+		uni.showToast({
+			title: '已中止',
+			icon: 'none',
+			duration: 1500
+		});
+	}
+	
+	// 重新生成回复
+	const regenerateMessage = () => {
+		if (!lastUserQuestion.value || isProcessing.value) return;
+		
+		// 删除最后一条 AI 消息
+		if (chatMessages.value.length > 0 && chatMessages.value[chatMessages.value.length - 1].sender === 'ai') {
+			chatMessages.value.pop();
+		}
+		
+		// 添加新的正在输入消息
+		const typingMessageIndex = chatMessages.value.push({
+			sender: 'ai',
+			content: '',
+			time: getCurrentTime(),
+			timestamp: Date.now(),
+			isTyping: true
+		}) - 1;
+
+		currentTypingMessage.value = typingMessageIndex;
+		isProcessing.value = true;
+		messageQueue.value = [];
+		
+		// 重置 Thinking 模式状态
+		isInThinkingMode.value = false;
+		thinkingBuffer.value = '';
+		
+		scrollToBottom();
+		
+		// 重新发起请求
+		startSSERequest(lastUserQuestion.value);
+	}
+
+	// ========== 工具方法 ==========
+	
+	// 错误处理
+	const handleError = (error) => {
+		let errorMessage = '发生错误';
+		if (error.errMsg) {
+			errorMessage = error.errMsg;
+		} else if (error.message) {
+			errorMessage = error.message;
+		}
+
+		uni.showToast({
+			title: errorMessage,
+			icon: 'none',
+			duration: 2000
+		});
+	}
+
+	// 防抖函数
+	const debounce = (func, wait) => {
+		let timeout;
+		return (...args) => {
+			clearTimeout(timeout);
+			timeout = setTimeout(() => {
+				func.apply(null, args);
+			}, wait);
+		};
+	}
+	
+	// 输入框获取焦点
+	const onInputFocus = () => {
+		nextTick(() => {
+			scrollToBottom();
+		});
+	}
+	
+	// 滚动到底部
+	const scrollToBottom = () => {
+		nextTick(() => {
+			const query = uni.createSelectorQuery();
+			query.select('.chat-list').boundingClientRect(data => {
+				if (data) {
+					scrollTop.value = data.height + 1000;
 				}
-				
-				// 重置 Thinking 模式状态
-				this.isInThinkingMode = false;
-				this.thinkingBuffer = '';
-				
-				this.isProcessing = false;
-				this.handleError({ message: error || '网络异常,请稍后重试' });
-			},
+			}).exec();
+		});
+	}
+	
+	const onScroll = (e) => {
+		// 可以添加滚动事件处理
+	}
+	
+	const getCurrentTime = () => {
+		return getFormattedTime(new Date());
+	}
+	
+	const getFormattedTime = (date) => {
+		const hours = date.getHours().toString().padStart(2, '0');
+		const minutes = date.getMinutes().toString().padStart(2, '0');
+		return `${hours}:${minutes}`;
+	}
+	
+	const useQuestion = (question) => {
+		inputMessage.value = question;
+	}
+	
+	const containsKeywords = (text) => {
+		const keywords = ['水稻', '小麦', '玉米', '病虫害', '农药', '化肥', '有机肥', '种植技术'];
+		return keywords.some(keyword => text.includes(keyword));
+	}
+	
+	const formatMessage = (text) => {
+		// 将文本中的换行符转换为<br>标签
+		return text.replace(/\n/g, '<br>');
+	}
+	
+	const initMessages = () => {
+		// 确保消息有时间戳
+		chatMessages.value.forEach(msg => {
+			if (!msg.timestamp) {
+				msg.timestamp = new Date().getTime();
+			}
+		});
 
-			// ========== 用户交互方法 ==========
+		// 按时间排序
+		chatMessages.value.sort((a, b) => a.timestamp - b.timestamp);
+	}
+	
+	const showDateSeparator = (index) => {
+		// 判断是否需要显示日期分割线
+		if (index === 0) return true;
+
+		const currentMsg = chatMessages.value[index];
+		const prevMsg = chatMessages.value[index - 1];
+
+		// 如果两条消息相隔超过30分钟,或者是不同日期,显示日期分割线
+		return isDifferentDay(currentMsg.timestamp, prevMsg.timestamp) ||
+			(currentMsg.timestamp - prevMsg.timestamp > 30 * 60 * 1000);
+	}
+	
+	const isDifferentDay = (timestamp1, timestamp2) => {
+		const date1 = new Date(timestamp1);
+		const date2 = new Date(timestamp2);
+
+		return date1.getDate() !== date2.getDate() ||
+			date1.getMonth() !== date2.getMonth() ||
+			date1.getFullYear() !== date2.getFullYear();
+	}
+	
+	const formatDateSeparator = (timestamp) => {
+		const now = new Date();
+		const msgDate = new Date(timestamp);
+
+		// 今天
+		if (isSameDay(msgDate, now)) {
+			return '今天 ' + getFormattedTime(msgDate);
+		}
+
+		// 昨天
+		const yesterday = new Date(now);
+		yesterday.setDate(now.getDate() - 1);
+		if (isSameDay(msgDate, yesterday)) {
+			return '昨天 ' + getFormattedTime(msgDate);
+		}
+
+		// 一周内
+		const oneWeekAgo = new Date(now);
+		oneWeekAgo.setDate(now.getDate() - 7);
+		if (msgDate >= oneWeekAgo) {
+			const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
+			return weekdays[msgDate.getDay()] + ' ' + getFormattedTime(msgDate);
+		}
+
+		// 其他日期
+		return msgDate.getFullYear() + '年' + (msgDate.getMonth() + 1) + '月' + msgDate.getDate() + '日 ' + getFormattedTime(msgDate);
+	}
+	
+	const isSameDay = (date1, date2) => {
+		return date1.getDate() === date2.getDate() &&
+			date1.getMonth() === date2.getMonth() &&
+			date1.getFullYear() === date2.getFullYear();
+	}
+	
+	// 获取下一轮建议问题列表
+	const fetchSuggestedQuestions = async (data) => {
+		console.log("获取下一轮建议问题参数",data);
+		try {
+			const response = await chatStreamSuggested(data);
 			
-			// 发送消息
-			submitQuestion() {
-				if (!storage.getHasLogin()) {
-					uni.showModal({
-						title: '提示',
-						content: '您还未登录,请先登录',
-						confirmText: '去登录',
-						cancelText: '取消',
-						success: function(res) {
-							if (res.confirm) {
-								uni.navigateTo({
-									url: '/pages/login/index'
-								});
-							}
-						},
-					});
-					return;
+			if (response && response.data) {
+				// 假设接口返回的数据格式为 { data: ["问题1", "问题2", ...] }
+				if (Array.isArray(response.data) && response.data.length > 0) {
+					suggestedQuestions.value = response.data;
+				} else if (response.data.questions && Array.isArray(response.data.questions)) {
+					// 或者接口返回 { data: { questions: [...] } }
+					suggestedQuestions.value = response.data.questions;
 				}
-				
-				if (!this.inputMessage.trim() || this.isProcessing) return;
+			}
+		} catch (error) {
+			console.error('获取建议问题失败:', error);
+			// 失败时保持默认建议问题,不影响用户体验
+		}
+	}
+	
+	// 生命周期钩子
+	onMounted(() => {
+		// 初始化防抖函数
+		debouncedSubmitQuestion = debounce(submitQuestion, 300)
+		
+		// 获取系统信息
+		const systemInfo = uni.getSystemInfoSync();
+		statusBarHeight.value = systemInfo.statusBarHeight || 20;
+		isIOS.value = systemInfo.platform === 'ios';
+		safeAreaBottom.value = systemInfo.safeAreaInsets ? (systemInfo.safeAreaInsets.bottom || 0) : 0;
+
+		// 初始化消息
+		initMessages();
+
+		// 滚动到底部
+		nextTick(() => {
+			scrollToBottom();
+		});
+		
+		// 监听来自 renderjs 的自定义事件
+		window.addEventListener('renderjs-stream-data', (event) => {
+			console.log('通过事件接收到数据:', event.detail);
+			onStreamData(event.detail);
+		});
+	})
+	
+	// 组件销毁时清理资源
+	onBeforeUnmount(() => {
+		// 停止打字机效果
+		stopTypingEffect();
+		
+		// 通知 renderjs 停止连接
+		if (isProcessing.value) {
+			renderjsData.value = {
+				action: 'stop',
+				timestamp: Date.now()
+			};
+		}
+		
+		// 清理消息队列
+		messageQueue.value = [];
+		
+		// 移除事件监听器
+		window.removeEventListener('renderjs-stream-data', onStreamData);
+	})
+</script>
 
-				const question = this.inputMessage.trim();
-				this.lastUserQuestion = question; // 保存问题用于重新生成
-				this.inputMessage = '';
-				
-				// 添加用户消息
-				this.chatMessages.push({
-					sender: 'user',
-					content: question,
-					time: this.getCurrentTime(),
-					timestamp: Date.now()
-				});
-
-				// 添加 AI 正在输入的消息
-				const typingMessageIndex = this.chatMessages.push({
-					sender: 'ai',
-					content: '',
-					time: this.getCurrentTime(),
-					timestamp: Date.now(),
-					isTyping: true
-				}) - 1;
-
-				this.currentTypingMessage = typingMessageIndex;
-				this.isProcessing = true;
-				this.scrollToBottom();
-				
-				// 通过 renderjs 发起 SSE 请求
-				this.startSSERequest(question);
-			},
+<script module="renderModule" lang="renderjs">
+	// renderjs 模块状态
+	let eventSource = null;
+	let reader = null;
+	let isReading = false;
+	let eventType = '';
+	let dataBuffer = [];
+	let messageIdS = null;
+	
+	// 启动 SSE 连接
+	async function startSSE(config, ownerInstance) {
+		// 先停止之前的连接
+		stopSSE();
+		
+		const { url, data, token } = config;
+		
+		try {
+			// 使用 fetch API 建立 SSE 连接
+			const response = await fetch(url, {
+				method: 'POST',
+				headers: {
+					'Content-Type': 'application/json',
+					'Accept': 'text/event-stream',
+					'Authorization': `Bearer ${token}`
+				},
+				body: JSON.stringify(data)
+			});
 			
-			// 启动 SSE 请求(通过 renderjs)
-			startSSERequest(question) {
-				const url = api.serve + '/uniapp/dify/chat/stream';
-				this.sessionId = Date.now().toString()
-				const requestData = {
-					query: question,
-					user: 'user_' + this.sessionId
-				};
-				
-				// 更新 renderjs 数据,触发 SSE 连接
-				this.renderjsData = {
-					action: 'start',
-					url: url,
-					data: requestData,
-					token: storage.getAccessToken(),
-					timestamp: Date.now()
-				};
-			},
+			if (!response.ok) {
+				throw new Error(`HTTP error! status: ${response.status}`);
+			}
 			
-			// 停止流式输出
-			stopStreaming() {
-				console.log('用户中止流式输出');
-				
-				// 通知 renderjs 停止
-				this.renderjsData = {
-					action: 'stop',
-					timestamp: Date.now()
-				};
-				
-				// 立即停止打字机效果并完成
-				this.finishStreaming();
-				
-				uni.showToast({
-					title: '已中止',
-					icon: 'none',
-					duration: 1500
-				});
-			},
+			// 获取 reader
+			reader = response.body.getReader();
+			const decoder = new TextDecoder('utf-8');
+			isReading = true;
 			
-			// 重新生成回复
-			regenerateMessage() {
-				if (!this.lastUserQuestion || this.isProcessing) return;
+			let buffer = '';
+			eventType = '';          // 当前事件名
+			dataBuffer = [];         // 当前事件的所有 data 行
+			
+			// 读取流式数据
+			while (isReading) {
+				const { done, value } = await reader.read();
 				
-				// 删除最后一条 AI 消息
-				if (this.chatMessages.length > 0 && this.chatMessages[this.chatMessages.length - 1].sender === 'ai') {
-					this.chatMessages.pop();
+				if (done) {
+					console.log('SSE 流结束');
+					window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
+						detail: { type: 'end', id: messageIdS }
+					}));
+					break;
 				}
 				
-				// 添加新的正在输入消息
-				const typingMessageIndex = this.chatMessages.push({
-					sender: 'ai',
-					content: '',
-					time: this.getCurrentTime(),
-					timestamp: Date.now(),
-					isTyping: true
-				}) - 1;
-
-				this.currentTypingMessage = typingMessageIndex;
-				this.isProcessing = true;
-				this.messageQueue = [];
-				
-				// 重置 Thinking 模式状态
-				this.isInThinkingMode = false;
-				this.thinkingBuffer = '';
+				// 解码数据
+				buffer += decoder.decode(value, { stream: true });
 				
-				this.scrollToBottom();
+				// 按行处理
+				const lines = buffer.split('\n');
+				buffer = lines.pop() || ''; // 保留最后不完整的行
 				
-				// 重新发起请求
-				this.startSSERequest(this.lastUserQuestion);
-			},
-
-			// ========== 工具方法 ==========
-			
-			// 错误处理
-			handleError(error) {
-				let errorMessage = '发生错误';
-				if (error.errMsg) {
-					errorMessage = error.errMsg;
-				} else if (error.message) {
-					errorMessage = error.message;
+				for (const line of lines) {
+					processLine(line, ownerInstance);
 				}
-
-				uni.showToast({
-					title: errorMessage,
-					icon: 'none',
-					duration: 2000
-				});
-			},
-
-			// 防抖函数
-			debounce(func, wait) {
-				let timeout;
-				return (...args) => {
-					clearTimeout(timeout);
-					timeout = setTimeout(() => {
-						func.apply(this, args);
-					}, wait);
-				};
-			},
-			
-			// 输入框获取焦点
-			onInputFocus() {
-				this.$nextTick(() => {
-					this.scrollToBottom();
-				});
-			},
-			
-			// 滚动到底部
-			scrollToBottom() {
-				this.$nextTick(() => {
-					const query = uni.createSelectorQuery().in(this);
-					query.select('.chat-list').boundingClientRect(data => {
-						if (data) {
-							this.scrollTop = data.height + 1000;
-						}
-					}).exec();
-				});
-			},
-			onScroll(e) {
-				// 可以添加滚动事件处理
-			},
-			getCurrentTime() {
-				return this.getFormattedTime(new Date());
-			},
-			getFormattedTime(date) {
-				const hours = date.getHours().toString().padStart(2, '0');
-				const minutes = date.getMinutes().toString().padStart(2, '0');
-				return `${hours}:${minutes}`;
-			},
+			}
 			
-			useQuestion(question) {
-				this.inputMessage = question;
-			},
-			containsKeywords(text) {
-				const keywords = ['水稻', '小麦', '玉米', '病虫害', '农药', '化肥', '有机肥', '种植技术'];
-				return keywords.some(keyword => text.includes(keyword));
-			},
-			formatMessage(text) {
-				// 将文本中的换行符转换为<br>标签
-				return text.replace(/\n/g, '<br>');
-			},
-			initMessages() {
-				// 确保消息有时间戳
-				this.chatMessages.forEach(msg => {
-					if (!msg.timestamp) {
-						msg.timestamp = new Date().getTime();
-					}
-				});
-
-				// 按时间排序
-				this.chatMessages.sort((a, b) => a.timestamp - b.timestamp);
-			},
-			showDateSeparator(index) {
-				// 判断是否需要显示日期分割线
-				if (index === 0) return true;
-
-				const currentMsg = this.chatMessages[index];
-				const prevMsg = this.chatMessages[index - 1];
-
-				// 如果两条消息相隔超过30分钟,或者是不同日期,显示日期分割线
-				return this.isDifferentDay(currentMsg.timestamp, prevMsg.timestamp) ||
-					(currentMsg.timestamp - prevMsg.timestamp > 30 * 60 * 1000);
-			},
-			isDifferentDay(timestamp1, timestamp2) {
-				const date1 = new Date(timestamp1);
-				const date2 = new Date(timestamp2);
-
-				return date1.getDate() !== date2.getDate() ||
-					date1.getMonth() !== date2.getMonth() ||
-					date1.getFullYear() !== date2.getFullYear();
-			},
-			formatDateSeparator(timestamp) {
-				const now = new Date();
-				const msgDate = new Date(timestamp);
-
-				// 今天
-				if (this.isSameDay(msgDate, now)) {
-					return '今天 ' + this.getFormattedTime(msgDate);
-				}
-
-				// 昨天
-				const yesterday = new Date(now);
-				yesterday.setDate(now.getDate() - 1);
-				if (this.isSameDay(msgDate, yesterday)) {
-					return '昨天 ' + this.getFormattedTime(msgDate);
+		} catch (error) {
+			console.error('SSE 连接错误:', error);
+			window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
+				detail: { 
+					type: 'error', 
+					error: error.message || '连接失败' 
 				}
-
-				// 一周内
-				const oneWeekAgo = new Date(now);
-				oneWeekAgo.setDate(now.getDate() - 7);
-				if (msgDate >= oneWeekAgo) {
-					const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
-					return weekdays[msgDate.getDay()] + ' ' + this.getFormattedTime(msgDate);
-				}
-
-				// 其他日期
-				return msgDate.getFullYear() + '年' + (msgDate.getMonth() + 1) + '月' + msgDate.getDate() + '日 ' +
-					this
-					.getFormattedTime(msgDate);
-			},
-			isSameDay(date1, date2) {
-				return date1.getDate() === date2.getDate() &&
-					date1.getMonth() === date2.getMonth() &&
-					date1.getFullYear() === date2.getFullYear();
-			},
+			}));
+		} finally {
+			stopSSE();
+		}	
+	}
+	
+	// 处理单行数据
+	function processLine(line, ownerInstance) {
+		if (!line.trim()) return;
+		
+		console.log('[renderjs] 处理行数据:', line);
+		
+		// 解析 SSE 格式
+		if (line.startsWith('event:')) {
+			// event: message
+			return;
+		}
+		
+		if (line.startsWith('data:') || line !== '') {
+			let data = line;
+			if (line.startsWith('data:')) {
+				data = data.substring(5).trim();
+			}
+			 			
+			if (!data || data.includes("ping")) return;
 			
-			// 获取下一轮建议问题列表
-			async fetchSuggestedQuestions(data) {
-				console.log("获取下一轮建议问题参数",data);
+			console.log('[renderjs] 解析后的数据:', data);
+			
+			// 过滤掉 MESSAGE_END 等元数据事件(JSON 格式)
+			if (data.startsWith('{') && data.includes('"eventType"')) {
 				try {
-					const response = await chatStreamSuggested(data);
-					
-					if (response && response.data) {
-						// 假设接口返回的数据格式为 { data: ["问题1", "问题2", ...] }
-						if (Array.isArray(response.data) && response.data.length > 0) {
-							this.suggestedQuestions = response.data;
-						} else if (response.data.questions && Array.isArray(response.data.questions)) {
-							// 或者接口返回 { data: { questions: [...] } }
-							this.suggestedQuestions = response.data.questions;
-						}
+					const jsonData = JSON.parse(data);
+					console.log('[renderjs] JSON 事件:', jsonData);
+					// 如果是 MESSAGE_END 事件,通知结束
+					if (jsonData.eventType === 'MESSAGE_END' || jsonData.event === 'message_end') {
+						console.log('[renderjs] 收到 MESSAGE_END 事件,流式结束');
+						messageIdS = jsonData.id;
+						window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
+							detail: { type: 'end', id: messageIdS }
+						}));
+						return;
 					}
-				} catch (error) {
-					console.error('获取建议问题失败:', error);
-					// 失败时保持默认建议问题,不影响用户体验
+					// 其他元数据事件也忽略
+					return;
+				} catch (e) {
+					console.log('[renderjs] JSON 解析失败,作为普通文本处理');
+					// 不是 JSON,继续处理
 				}
 			}
-		},
-		// 组件销毁时清理资源
-		beforeDestroy() {
-			// 停止打字机效果
-			this.stopTypingEffect();
 			
-			// 通知 renderjs 停止连接
-			if (this.isProcessing) {
-				this.renderjsData = {
-					action: 'stop',
-					timestamp: Date.now()
-				};
+			// 检查是否是 Thinking 内容
+			if (data.includes('<details') && data.includes('<summary>')) {
+				console.log('[renderjs] 发送 thinking 类型数据');
+				// 使用自定义事件发送数据
+				window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
+					detail: {
+						type: 'thinking',
+						content: data
+					}
+				}));
+			} else {
+				console.log('[renderjs] 发送 message 类型数据,长度:', data.length);
+				// 普通消息内容
+				window.dispatchEvent(new CustomEvent('renderjs-stream-data', {
+					detail: {
+						type: 'message',
+						content: data
+					}
+				}));
 			}
-			
-			// 清理消息队列
-			this.messageQueue = [];
 		}
 	}
-</script>
-
-<!-- renderjs 模块:处理 H5/App 端的 SSE 流式连接 -->
-<script module="renderModule" lang="renderjs">
+	
+	// 停止 SSE 连接
+	function stopSSE() {
+		isReading = false;
+		
+		if (reader) {
+			try {
+				reader.cancel();
+			} catch (e) {
+				console.error('关闭 reader 失败:', e);
+			}
+			reader = null;
+		}
+		
+		if (eventSource) {
+			eventSource.close();
+			eventSource = null;
+		}
+	}
+	
+	// 导出方法供 Vue 调用
 	export default {
-		data() {
-			return {
-				eventSource: null,
-				reader: null,
-				isReading: false
-			};
-		},
 		methods: {
 			// 监听 prop 变化
-			onDataChange(newValue, oldValue, ownerInstance, instance) {
+			onDataChange(newValue, oldValue, ownerInstance) {
 				if (!newValue || !newValue.action) return;
 				
 				if (newValue.action === 'start') {
-					this.startSSE(newValue, ownerInstance);
+					startSSE(newValue, ownerInstance);
 				} else if (newValue.action === 'stop') {
-					this.stopSSE();
-				}
-			},
-			
-			// 启动 SSE 连接
-			async startSSE(config, ownerInstance) {
-				// 先停止之前的连接
-				this.stopSSE();
-				
-				const { url, data, token } = config;
-				
-				try {
-					// 使用 fetch API 建立 SSE 连接
-					const response = await fetch(url, {
-						method: 'POST',
-						headers: {
-							'Content-Type': 'application/json',
-							'Accept': 'text/event-stream',
-							'Authorization': `Bearer ${token}`
-						},
-						body: JSON.stringify(data)
-					});
-					
-					if (!response.ok) {
-						throw new Error(`HTTP error! status: ${response.status}`);
-					}
-					
-					// 获取 reader
-					this.reader = response.body.getReader();
-					const decoder = new TextDecoder('utf-8');
-					this.isReading = true;
-					
-					let buffer = '';
-					this.eventType    = '';          // 当前事件名
-					this.dataBuffer   = [];          // 当前事件的所有 data 行
-					
-					// 读取流式数据
-					while (this.isReading) {
-						const { done, value } = await this.reader.read();
-						
-						if (done) {
-							console.log('SSE 流结束');
-							ownerInstance.callMethod('onStreamData', { type: 'end' , id: this.messageId});
-							break;
-						}
-						
-						// 解码数据
-						buffer += decoder.decode(value, { stream: true });
-						
-						// 按行处理
-						const lines = buffer.split('\n');
-						buffer = lines.pop() || ''; // 保留最后不完整的行						
-						// buffer = lines.pop(); // 保留最后不完整的行
-						
-						for (const line of lines) {
-							this.processLine(line, ownerInstance);
-						}
-					}
-					
-				} catch (error) {
-					console.error('SSE 连接错误:', error);
-					ownerInstance.callMethod('onStreamData', { 
-						type: 'error', 
-						error: error.message || '连接失败' 
-					});
-				} finally {
-					this.stopSSE();
-				}	
-			},
-			// 处理单行数据
-			processLine(line, ownerInstance) {
-				// console.log("处理单行数据:",line);
-				if (!line.trim()) return;
-				
-				// 解析 SSE 格式
-				if (line.startsWith('event:')) {
-					// event: message
-					return;
-				}
-				
-				if (line.startsWith('data:') || line != '') {
-					let data = line;
-					if (line.startsWith('data:') ){
-						data = data.substring(5).trim();
-					}
-					 			
-					if (!data || data.includes("ping") ) return;
-					
-					// 过滤掉 MESSAGE_END 等元数据事件(JSON 格式)
-					if (data.startsWith('{') && data.includes('"eventType"')) {
-						try {
-							const jsonData = JSON.parse(data);
-							// 如果是 MESSAGE_END 事件,通知结束
-							if (jsonData.eventType === 'MESSAGE_END' || jsonData.event === 'message_end') {
-								console.log('收到 MESSAGE_END 事件,流式结束');
-								this.messageId = jsonData.id
-								ownerInstance.callMethod('onStreamData', { type: 'end' , id: this.messageId });
-								return;
-							}
-							// 其他元数据事件也忽略
-							return;
-						} catch (e) {
-							// 不是 JSON,继续处理
-						}
-					}
-					
-					// 检查是否是 Thinking 内容
-					if (data.includes('<details') && data.includes('<summary>')) {
-						ownerInstance.callMethod('onStreamData', {
-							type: 'thinking',
-							content: data
-						});
-					} else {
-						// 普通消息内容
-						ownerInstance.callMethod('onStreamData', {
-							type: 'message',
-							content: data
-						});
-					}
-				}
-			},
-			
-			// 停止 SSE 连接
-			stopSSE() {
-				this.isReading = false;
-				
-				if (this.reader) {
-					try {
-						this.reader.cancel();
-					} catch (e) {
-						console.error('关闭 reader 失败:', e);
-					}
-					this.reader = null;
-				}
-				
-				if (this.eventSource) {
-					this.eventSource.close();
-					this.eventSource = null;
+					stopSSE();
 				}
 			}
 		}

+ 0 - 0
README.md → pages/knowledge/ai-chat/index_vue3.vue


+ 324 - 326
pages/knowledge/detail.vue

@@ -119,363 +119,361 @@
   </view>
 </template>
 
-<script>
-	import {getKnowledgeDetail,getKnowledgeView} from "@/api/services/knowledge.js";
-export default {
-  data() {
-    return {
-      title: "农业知识",
-      loading: true,
-      articleInfo: null,
-      knowledgeImages: [], // 文章关联的图片
-      currentSwiperIndex: 0,
-      isH5: false, // 是否是H5环境
-      videoPlaying: false,
-      id: null,
-      type: '',
-	  carouselImages: [],
-	  currentSwiperIndex: 0,
-    };
-  },
-  computed: {
-    processedContent() {
-      console.log("this.articleInfo",this.articleInfo);
-      // 检查文章内容是否存在于article_content或content_category字段
-      if (!this.articleInfo) return '';
-      
-      // 优先使用content_category字段,如果不存在则尝试使用article_content字段
-      let content = this.articleInfo.content_category || this.articleInfo.articleContent || '';
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { getKnowledgeDetail, getKnowledgeView } from "@/api/services/knowledge.js"
+import { onShow, onLoad, onNavigationBarButtonTap} from '@dcloudio/uni-app'
+
+// 响应式数据
+const title = ref("农业知识")
+const loading = ref(true)
+const articleInfo = ref(null)
+const knowledgeImages = ref([]) // 文章关联的图片
+const currentSwiperIndex = ref(0)
+const isH5 = ref(false) // 是否是H5环境
+const videoPlaying = ref(false)
+const id = ref(null)
+const type = ref('')
+const carouselImages = ref([])
+
+// 计算属性
+// 计算属性
+const processedContent = computed(() => {
+  console.log("articleInfo", articleInfo.value)
+  // 检查文章内容是否存在于article_content或content_category字段
+  if (!articleInfo.value) return ''
+  
+  // 优先使用content_category字段,如果不存在则尝试使用article_content字段
+  let content = articleInfo.value.content_category || articleInfo.value.articleContent || ''
+  
+  // 如果内容已经包含HTML标签
+  if (content.includes('<') && content.includes('>')) {
+    // 增强绿色背景区块显示
+    content = content.replace(/<div[^>]*>水稻秧苗培育<\/div>/g, '<div class="green-block">水稻秧苗培育</div>')
+    content = content.replace(/<div[^>]*>水稻收获与储存<\/div>/g, '<div class="green-block">水稻收获与储存</div>')
+    
+    // 增强注意事项样式
+    content = content.replace(/<div[^>]*>\s*\[注意事项\]\s*(.*?)<\/div>/gs, '<div class="notice-box"><div class="notice-content">$1</div></div>')
+    
+    // 识别H3标题并在其后插入对应的图片
+    if (knowledgeImages.value && knowledgeImages.value.length > 0) {
+      // 为每个标题后添加对应的图片
+      const titleRegex = /<h[1-3][^>]*>(.*?)<\/h[1-3]>/g
+      let titleMatch
+      let imageIndex = 0
+      let titleCount = 0
+      let imageHtml = ''
+      let matches = []
       
-      // 如果内容已经包含HTML标签
-      if (content.includes('<') && content.includes('>')) {
-        // 增强绿色背景区块显示
-        content = content.replace(/<div[^>]*>水稻秧苗培育<\/div>/g, '<div class="green-block">水稻秧苗培育</div>');
-        content = content.replace(/<div[^>]*>水稻收获与储存<\/div>/g, '<div class="green-block">水稻收获与储存</div>');
-        
-        // 增强注意事项样式
-        content = content.replace(/<div[^>]*>\s*\[注意事项\]\s*(.*?)<\/div>/gs, '<div class="notice-box"><div class="notice-content">$1</div></div>');
-        
-        // 识别H3标题并在其后插入对应的图片
-        if (this.knowledgeImages && this.knowledgeImages.length > 0) {
-          // 为每个标题后添加对应的图片
-          const titleRegex = /<h[1-3][^>]*>(.*?)<\/h[1-3]>/g;
-          let titleMatch;
-          let imageIndex = 0;
-          let titleCount = 0;
-          let imageHtml = '';
-          let matches = [];
-          
-          // 先计算有多少个标题
-          while ((titleMatch = titleRegex.exec(content)) !== null) {
-            matches.push({
-              fullMatch: titleMatch[0],
-              title: titleMatch[1],
-              index: titleMatch.index
-            });
-          }
-          
-          // 处理每个标题,插入相应的图片
-          for (let i = matches.length - 1; i >= 0; i--) {
-            if (imageIndex < this.knowledgeImages.length) {
-              const image = this.knowledgeImages[imageIndex];
-			  console.log("image打打符:",image);
-              // 根据是否有真实图片URL来决定显示方式
-              imageHtml = image.url? 
-              `
-                <div class="content-image-wrapper">
-                  <div class="content-image-real" style="background-image: url('${image.url}');">
-                    
-                  </div>
-                </div>
-              ` : 
-              `
-                <div class="content-image-wrapper">
-                  <div class="content-image" style="background-color: ${image.color || '#8BC34A'};">
-                    <div class="image-text">${image.title || '农业知识图解'}</div>
-                  </div>
-                </div>
-              `;
-              content = content.substring(0, matches[i].index + matches[i].fullMatch.length) + imageHtml + content.substring(matches[i].index + matches[i].fullMatch.length);
-              imageIndex++;
-            }
-          }
-        }
-        
-        return content;
+      // 先计算有多少个标题
+      while ((titleMatch = titleRegex.exec(content)) !== null) {
+        matches.push({
+          fullMatch: titleMatch[0],
+          title: titleMatch[1],
+          index: titleMatch.index
+        })
       }
       
-      // 如果是纯文本内容,则增强格式化
-      // 替换\n为<br>标签
-      content = content.replace(/\n/g, '<br>');
-      
-      // 识别章节标题
-      content = content.replace(/([一二三四五六七八九十])、\s*([^\n<]+)/g, '<h3>$1、$2</h3>');
-      
-      // 识别注意事项段落
-      content = content.replace(/\[注意事项\]\s*(.*?)(\n|$)/g, '<div class="notice-box"><div class="notice-content">$1</div></div>');
-      
-      // 在标题后插入对应图片
-      if (this.knowledgeImages && this.knowledgeImages.length > 0) {
-        const sections = content.split(/<h3>/);
-        let newContent = sections[0]; // 保留第一部分
-        
-        for (let i = 1; i < sections.length; i++) {
-          newContent += '<h3>' + sections[i];
-          
-          // 如果有对应的图片,在标题后插入图片
-          if (i - 1 < this.knowledgeImages.length) {
-            const image = this.knowledgeImages[i - 1];
-			console.log("image打打符:",image);
-            // 根据是否有真实图片URL来决定显示方式
-            newContent += image.url ? 
-            `
-              <div class="content-image-wrapper">
-                <div class="content-image-real" style="background-image: url('${image.url}');">
-                  <div class="image-text-overlay">
-                    <div class="image-text">${image.title || '农业知识图解'}</div>
-                  </div>
-                </div>
+      // 处理每个标题,插入相应的图片
+      for (let i = matches.length - 1; i >= 0; i--) {
+        if (imageIndex < knowledgeImages.value.length) {
+          const image = knowledgeImages.value[imageIndex]
+          console.log("image打打符:", image)
+          // 根据是否有真实图片URL来决定显示方式
+          imageHtml = image.url? 
+          `
+            <div class="content-image-wrapper">
+              <div class="content-image-real" style="background-image: url('${image.url}');">
+                
               </div>
-            ` : 
-            `
-              <div class="content-image-wrapper">
-                <div class="content-image" style="background-color: ${image.color || '#8BC34A'};">
-                  <div class="image-text">${image.title || '农业知识图解'}</div>
-                </div>
+            </div>
+          ` : 
+          `
+            <div class="content-image-wrapper">
+              <div class="content-image" style="background-color: ${image.color || '#8BC34A'};">
+                <div class="image-text">${image.title || '农业知识图解'}</div>
               </div>
-            `;
-          }
+            </div>
+          `
+          content = content.substring(0, matches[i].index + matches[i].fullMatch.length) + imageHtml + content.substring(matches[i].index + matches[i].fullMatch.length)
+          imageIndex++
         }
-        
-        content = newContent;
       }
-      
-      // 包装段落
-      content = `<div style="font-size: 30rpx; line-height: 1.8; color: #333; margin-bottom: 20rpx;">${content}</div>`;
-      
-      return content;
     }
-  },
-  onLoad(options) {
-    // 获取传递的参数
-    this.id = Number(options.id);
-    this.type = options.type;
-    
-    // 检测是否在H5环境中运行
-    // #ifdef H5
-    this.isH5 = true;
-    // #endif
-	console.log("this.isH5",this.isH5);
     
-    // 设置导航栏标题
-    uni.setNavigationBarTitle({
-      title: this.type === 'tech' ? '农技知识' : '政策解读'
-    });
+    return content
+  }
+  
+  // 如果是纯文本内容,则增强格式化
+  // 替换\n为<br>标签
+  content = content.replace(/\n/g, '<br>')
+  
+  // 识别章节标题
+  content = content.replace(/([一二三四五六七八九十])、\s*([^\n<]+)/g, '<h3>$1、$2</h3>')
+  
+  // 识别注意事项段落
+  content = content.replace(/\[注意事项\]\s*(.*?)(\n|$)/g, '<div class="notice-box"><div class="notice-content">$1</div></div>')
+  
+  // 在标题后插入对应图片
+  if (knowledgeImages.value && knowledgeImages.value.length > 0) {
+    const sections = content.split(/<h3>/)
+    let newContent = sections[0] // 保留第一部分
     
-    // 加载文章详情
-    this.fetchArticleDetail();
-  },
-  onNavigationBarButtonTap(e) {
-    if (e.index === 0) {
-      this.goBack();
-    }
-  },
-  methods: {
-    // 获取文章详情数据
-    async fetchArticleDetail() {
-      this.loading = true;
+    for (let i = 1; i < sections.length; i++) {
+      newContent += '<h3>' + sections[i]
       
-      try {
-       await getKnowledgeDetail({
-            id: this.id,
-            type: this.type
-          }).then(response =>{
-		   if (response.data.data && response.data.code === 200) {
-		     this.articleInfo = response.data.data;
-		     
-		     // 设置默认的点赞和收藏状态
-		     this.articleInfo.liked = false;
-		     this.articleInfo.collected = false;
-		     
-		     // 处理文章关联的图片
-		     if (this.articleInfo.knowledgeImage && this.articleInfo.knowledgeImage.length > 0) {
-			this.articleInfo.knowledgeImage.forEach(image => {
-			    // 检查 imageType 是否为 'carousel'
-			    if (image.imageType === 'carousel') {
-			        // 如果是,将该元素添加到 carouselImages 数组中
-			        this.carouselImages.push(image);
-			    }else{
-					this.knowledgeImages.push(image)
-				}
-				console.log("this.carouselImages",this.carouselImages);
-			});
-		       console.log('文章图片数据:', this.knowledgeImages);
-		     }
-		     // 增加浏览量
-		     this.updateViewCount();
-		   } else {
-		     uni.showToast({
-		       title: (response.data && response.data.message) || '获取文章详情失败',
-		       icon: 'none'
-		     });
-		   }
-	   }).catch(err => [err, null]);
-
-      } catch (error) {
-        console.error('获取文章详情失败:', error);
-        uni.showToast({
-          title: '网络异常,请稍后再试',
-          icon: 'none'
-        });
-      } finally {
-        this.loading = false;
-      }
-    },
-    
-    // 更新浏览量
-    async updateViewCount() {
-      try {
-		  getKnowledgeView({
-            id: this.id,
-            type: this.type
-          }).then(res=>{
-            if (res.data && res.data.code === 200) {
-              // 记录已更新的文章ID和新阅读量
-              if (this.articleInfo) {
-                // 增加阅读量计数,假设API成功更新了阅读量
-                this.articleInfo.viewCount = (this.articleInfo.viewCount || 0) + 1;
-                
-                // 发送全局事件通知列表页更新阅读量
-                uni.$emit('updateArticleViewCount', {
-                  id: this.id,
-                  type: this.type,
-                  viewCount: this.articleInfo.viewCount
-                });
-              }
-            }
-		  })
-
-      } catch (error) {
-        console.error('更新浏览量失败:', error);
+      // 如果有对应的图片,在标题后插入图片
+      if (i - 1 < knowledgeImages.value.length) {
+        const image = knowledgeImages.value[i - 1]
+        console.log("image打打符:", image)
+        // 根据是否有真实图片URL来决定显示方式
+        newContent += image.url ? 
+        `
+          <div class="content-image-wrapper">
+            <div class="content-image-real" style="background-image: url('${image.url}');">
+              <div class="image-text-overlay">
+                <div class="image-text">${image.title || '农业知识图解'}</div>
+              </div>
+            </div>
+          </div>
+        ` : 
+        `
+          <div class="content-image-wrapper">
+            <div class="content-image" style="background-color: ${image.color || '#8BC34A'};">
+              <div class="image-text">${image.title || '农业知识图解'}</div>
+            </div>
+          </div>
+        `
       }
-    },
+    }
     
-    // 处理点赞
-    async handleLike() {
-      try {
-        const action = this.articleInfo.liked ? 'unlike' : 'like';
-        
-        const [error, res] = await uni.request({
-          url: `http://localhost:8080/knowledge/${action}`, // 替换为实际的API地址
-          method: 'POST',
-          data: {
-            id: this.id,
-            type: this.type
-          }
-        }).catch(err => [err, null]);
+    content = newContent
+  }
+  
+  // 包装段落
+  content = `<div style="font-size: 30rpx; line-height: 1.8; color: #333; margin-bottom: 20rpx;">${content}</div>`
+  
+  return content
+})
+
+// 获取文章详情数据
+const fetchArticleDetail = async () => {
+  loading.value = true
+  
+  try {
+    await getKnowledgeDetail({
+      id: id.value,
+      type: type.value
+    }).then(response => {
+      if (response.data.data && response.data.code === 200) {
+        articleInfo.value = response.data.data
         
-        const response = res || {};
+        // 设置默认的点赞和收藏状态
+        articleInfo.value.liked = false
+        articleInfo.value.collected = false
         
-        if (response.data && response.data.code === 0) {
-          this.articleInfo.liked = !this.articleInfo.liked;
-        } else {
-          uni.showToast({
-            title: (response.data && response.data.message) || '操作失败',
-            icon: 'none'
-          });
+        // 处理文章关联的图片
+        if (articleInfo.value.knowledgeImage && articleInfo.value.knowledgeImage.length > 0) {
+          articleInfo.value.knowledgeImage.forEach(image => {
+            // 检查 imageType 是否为 'carousel'
+            if (image.imageType === 'carousel') {
+              // 如果是,将该元素添加到 carouselImages 数组中
+              carouselImages.value.push(image)
+            } else {
+              knowledgeImages.value.push(image)
+            }
+            console.log("carouselImages", carouselImages.value)
+          })
+          console.log('文章图片数据:', knowledgeImages.value)
         }
-      } catch (error) {
-        console.error('点赞操作失败:', error);
+        // 增加浏览量
+        updateViewCount()
+      } else {
         uni.showToast({
-          title: '网络异常,请稍后再试',
+          title: (response.data && response.data.message) || '获取文章详情失败',
           icon: 'none'
-        });
+        })
       }
-    },
-    
-    // 处理收藏
-    async handleCollect() {
-      try {
-        const action = this.articleInfo.collected ? 'uncollect' : 'collect';
-        
-        const [error, res] = await uni.request({
-          url: `http://localhost:8080/knowledge/${action}`, // 替换为实际的API地址
-          method: 'POST',
-          data: {
-            id: this.id,
-            type: this.type
-          }
-        }).catch(err => [err, null]);
-        
-        const response = res || {};
-        
-        if (response.data && response.data.code === 0) {
-          this.articleInfo.collected = !this.articleInfo.collected;
-        } else {
-          uni.showToast({
-            title: (response.data && response.data.message) || '操作失败',
-            icon: 'none'
-          });
+    }).catch(err => [err, null])
+  } catch (error) {
+    console.error('获取文章详情失败:', error)
+    uni.showToast({
+      title: '网络异常,请稍后再试',
+      icon: 'none'
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+// 更新浏览量
+const updateViewCount = async () => {
+  try {
+    getKnowledgeView({
+      id: id.value,
+      type: type.value
+    }).then(res => {
+      if (res.data && res.data.code === 200) {
+        // 记录已更新的文章ID和新阅读量
+        if (articleInfo.value) {
+          // 增加阅读量计数,假设API成功更新了阅读量
+          articleInfo.value.viewCount = (articleInfo.value.viewCount || 0) + 1
+          
+          // 发送全局事件通知列表页更新阅读量
+          uni.$emit('updateArticleViewCount', {
+            id: id.value,
+            type: type.value,
+            viewCount: articleInfo.value.viewCount
+          })
         }
-      } catch (error) {
-        console.error('收藏操作失败:', error);
-        uni.showToast({
-          title: '网络异常,请稍后再试',
-          icon: 'none'
-        });
       }
-    },
+    })
+  } catch (error) {
+    console.error('更新浏览量失败:', error)
+  }
+}
+
+// 处理点赞
+const handleLike = async () => {
+  try {
+    const action = articleInfo.value.liked ? 'unlike' : 'like'
     
-    // 播放视频
-    playVideo() {
-      this.videoPlaying = true;
-    },
+    const [error, res] = await uni.request({
+      url: `http://localhost:8080/knowledge/${action}`, // 替换为实际的API地址
+      method: 'POST',
+      data: {
+        id: id.value,
+        type: type.value
+      }
+    }).catch(err => [err, null])
     
-    // 格式化日期
-    formatDate(dateStr) {
-      if (!dateStr) return '';
-      
-      const date = new Date(dateStr);
-      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
-    },
+    const response = res || {}
     
-    // 获取随机颜色
-    getRandomColor() {
-      const colors = ['#8BC34A', '#4CAF50', '#7CB342', '#689F38', '#33691E'];
-      return colors[Math.floor(Math.random() * colors.length)];
-    },
+    if (response.data && response.data.code === 0) {
+      articleInfo.value.liked = !articleInfo.value.liked
+    } else {
+      uni.showToast({
+        title: (response.data && response.data.message) || '操作失败',
+        icon: 'none'
+      })
+    }
+  } catch (error) {
+    console.error('点赞操作失败:', error)
+    uni.showToast({
+      title: '网络异常,请稍后再试',
+      icon: 'none'
+    })
+  }
+}
+
+// 处理收藏
+const handleCollect = async () => {
+  try {
+    const action = articleInfo.value.collected ? 'uncollect' : 'collect'
     
-    // 根据类型获取图标
-    getIconByType() {
-      if (this.type === 'tech') {
-        return '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path><path d="M12 6a1 1 0 0 0-1 1v5a1 1 0 0 0 .55.89l4 2a1 1 0 0 0 .9-1.78L13 11.28V7a1 1 0 0 0-1-1z"></path></svg>';
-      } else {
-        return '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12h-4 M17 12l-3-3 M17 12l-3 3 M3 6h10 M13 6l-3-3 M13 6l-3 3 M3 18h10 M13 18l-3-3 M13 18l-3 3"></path></svg>';
+    const [error, res] = await uni.request({
+      url: `http://localhost:8080/knowledge/${action}`, // 替换为实际的API地址
+      method: 'POST',
+      data: {
+        id: id.value,
+        type: type.value
       }
-    },
-    
-    goBack() {
-      uni.navigateBack();
-    },
+    }).catch(err => [err, null])
     
-    scrollToTop() {
-		setTimeout(() => {
-			uni.pageScrollTo({scrollTop: 0, duration: 0});
-		}, 50);
-    },
+    const response = res || {}
     
-    handleShare() {
-      uni.showShareMenu({
-        withShareTicket: true,
-        menus: ['shareAppMessage', 'shareTimeline']
-      });
-    },
-    
-    handleSwiperChange(e) {
-      this.currentSwiperIndex = e.detail.current;
+    if (response.data && response.data.code === 0) {
+      articleInfo.value.collected = !articleInfo.value.collected
+    } else {
+      uni.showToast({
+        title: (response.data && response.data.message) || '操作失败',
+        icon: 'none'
+      })
     }
+  } catch (error) {
+    console.error('收藏操作失败:', error)
+    uni.showToast({
+      title: '网络异常,请稍后再试',
+      icon: 'none'
+    })
+  }
+}
+
+// 播放视频
+const playVideo = () => {
+  videoPlaying.value = true
+}
+
+// 格式化日期
+const formatDate = (dateStr) => {
+  if (!dateStr) return ''
+  
+  const date = new Date(dateStr)
+  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
+}
+
+// 获取随机颜色
+const getRandomColor = () => {
+  const colors = ['#8BC34A', '#4CAF50', '#7CB342', '#689F38', '#33691E']
+  return colors[Math.floor(Math.random() * colors.length)]
+}
+
+// 根据类型获取图标
+const getIconByType = () => {
+  if (type.value === 'tech') {
+    return '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path><path d="M12 6a1 1 0 0 0-1 1v5a1 1 0 0 0 .55.89l4 2a1 1 0 0 0 .9-1.78L13 11.28V7a1 1 0 0 0-1-1z"></path></svg>'
+  } else {
+    return '<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12h-4 M17 12l-3-3 M17 12l-3 3 M3 6h10 M13 6l-3-3 M13 6l-3 3 M3 18h10 M13 18l-3-3 M13 18l-3 3"></path></svg>'
   }
 }
+
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const scrollToTop = () => {
+  setTimeout(() => {
+    uni.pageScrollTo({ scrollTop: 0, duration: 0 })
+  }, 50)
+}
+
+const handleShare = () => {
+  uni.showShareMenu({
+    withShareTicket: true,
+    menus: ['shareAppMessage', 'shareTimeline']
+  })
+}
+
+const handleSwiperChange = (e) => {
+  currentSwiperIndex.value = e.detail.current
+}
+
+// uni-app 生命周期钩子
+onLoad((options) => {
+  // 获取传递的参数
+  id.value = Number(options.id)
+  type.value = options.type
+  
+  // 检测是否在H5环境中运行
+  // #ifdef H5
+  isH5.value = true
+  // #endif
+  console.log("isH5", isH5.value)
+  
+  // 设置导航栏标题
+  uni.setNavigationBarTitle({
+    title: type.value === 'tech' ? '农技知识' : '政策解读'
+  })
+  
+  // 加载文章详情
+  fetchArticleDetail()
+})
+
+onNavigationBarButtonTap((e) => {
+  if (e.index === 0) {
+    goBack()
+  }
+})
 </script>
 
 <style>

+ 266 - 282
pages/knowledge/index.vue

@@ -140,309 +140,293 @@
   </view>
 </template>
 
-<script>
-	import {getKnowledgeList} from "@/api/services/knowledge.js";
-export default {
-  data() {
-    return {
-      // 当前标签
-      currentTab: 0,
-      
-      // 农技知识列表
-      techList: [],
-      
-      // 政策解读列表
-      policyList: [],
-      
-      // 分页参数
-      techPageParams: {
-        page: 1,
-        pageSize: 10,
-        hasMore: true
-      },
-      
-      policyPageParams: {
-        page: 1,
-        pageSize: 10,
-        hasMore: true
-      },
-      
-      // 刷新加载状态
-      isRefreshing: false,
-      loading: false
-    };
-  },
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
+import { getKnowledgeList } from "@/api/services/knowledge.js"
+import { onShow } from '@dcloudio/uni-app'
+
+// 当前标签
+const currentTab = ref(0)
+
+// 农技知识列表
+const techList = ref([])
+
+// 政策解读列表
+const policyList = ref([])
+
+// 分页参数
+const techPageParams = reactive({
+  page: 1,
+  pageSize: 10,
+  hasMore: true
+})
+
+const policyPageParams = reactive({
+  page: 1,
+  pageSize: 10,
+  hasMore: true
+})
+
+// 刷新加载状态
+const isRefreshing = ref(false)
+const loading = ref(false)
+
+// 处理Tab切换
+const handleTabChange = (index) => {
+  if (currentTab.value === index) return
   
-  // 监听标签切换
-  // watch: {
-  //   currentTab(newVal) {
-  //     if (newVal === 0 && this.techList.length === 0) {
-  //       this.fetchTechKnowledge();
-  //     } else if (newVal === 1 && this.policyList.length === 0) {
-  //       this.fetchPolicyData();
-  //     }
-  //   }
-  // },
+  currentTab.value = index
+  console.log('Tab changed to:', index)
   
-  // 页面加载完成
-  mounted() {
-    console.log('Page mounted, current tab:', this.currentTab);
-    // 初始加载数据
-    this.fetchTechKnowledge();
-    
-    // 监听文章阅读量更新事件
-    uni.$on('updateArticleViewCount', this.handleViewCountUpdate);
-  },
+  // 如果切换到的标签没有数据,则加载数据
+  if (index === 0 && techList.value.length === 0) {
+    fetchTechKnowledge()
+  } else if (index === 1 && policyList.value.length === 0) {
+    fetchPolicyData()
+  }
+}
+
+// 获取农技知识数据
+const fetchTechKnowledge = async (refresh = false) => {
+  if (refresh) {
+    techPageParams.page = 1
+    techPageParams.hasMore = true
+  }
   
-  // 页面显示时触发
-  onShow() {
-    // 获取当前页面路由信息,判断是否是从详情页返回
-    const pages = getCurrentPages();
-    if (pages.length > 1) {
-      const prePage = pages[pages.length - 2];
-      // 如果前一个页面是详情页,检查是否需要刷新数据
-      if (prePage && prePage.route && prePage.route.includes('knowledge/detail')) {
-        console.log('从详情页返回,更新阅读量显示');
-        // 由于已经通过事件机制更新了列表,这里只需要触发一下视图更新
-        if (this.currentTab === 0) {
-          this.techList = [...this.techList];
-        } else {
-          this.policyList = [...this.policyList];
-        }
-      }
-    }
-  },
+  // 如果没有更多数据则不请求
+  if (!techPageParams.hasMore && !refresh) return
   
-  // 页面销毁前
-  beforeDestroy() {
-    // 移除事件监听
-    uni.$off('updateArticleViewCount', this.handleViewCountUpdate);
-  },
+  loading.value = true
   
-  methods: {
-    // 处理Tab切换
-    handleTabChange(index) {
-      if (this.currentTab === index) return;
-      
-      this.currentTab = index;
-      console.log('Tab changed to:', index);
-      
-      // 如果切换到的标签没有数据,则加载数据
-      if (index === 0 && this.techList.length === 0) {
-        this.fetchTechKnowledge();
-      } else if (index === 1 && this.policyList.length === 0) {
-        this.fetchPolicyData();
-      }
-    },
+  try {
+    // 构建请求参数
+    const params = {
+        pageNum: techPageParams.page,
+        pageSize: techPageParams.pageSize,
+        contentCategory: 'tech'
+    };
     
-    // 获取农技知识数据
-    async fetchTechKnowledge(refresh = false) {
-      if (refresh) {
-        this.techPageParams.page = 1;
-        this.techPageParams.hasMore = true;
-      }
-      
-      // 如果没有更多数据则不请求
-      if (!this.techPageParams.hasMore && !refresh) return;
-      
-      this.loading = true;
-      
-      try {
-        // 构建请求参数
-        const params = {
-          page: this.techPageParams.page,
-          pageSize: this.techPageParams.pageSize,
-          category: 'tech'
-        };
+    // 发起请求
+    await getKnowledgeList(params).then(response => {
+      console.log("response农技", response)
+      // 解析响应
+      if (response.data.data && response.data.code === 200) {
+        const { list, total } = response.data.data
         
-        // 发起请求
-        await getKnowledgeList(params).then(response=>{
-			console.log("response农技",response);
-			// 解析响应
-			if (response.data.data && response.data.code === 200) {
-			  const { list, total } = response.data.data;
-			  
-			  if (refresh) {
-			    this.techList = list || [];
-			  } else {
-			    this.techList = [...this.techList, ...(list || [])];
-			  }
-			  console.log("techList",this.techList);
-			  // 更新分页状态
-			  this.techPageParams.page++;
-			  this.techPageParams.hasMore = this.techList.length < total;
-			} else {
-			  uni.showToast({
-			    title: (response.data && response.data.message) || '获取数据失败',
-			    icon: 'none'
-			  });
-			}
-		}).catch(err => [err, null]);
-           
-      } catch (error) {
-        console.error('获取农技知识数据失败:', error);
-        uni.showToast({
-          title: '网络异常,请稍后再试',
-          icon: 'none'
-        });
-      } finally {
-        this.loading = false;
         if (refresh) {
-          this.isRefreshing = false;
-        }
-        
-        // 如果加载完成后没有更多数据,并且不是刷新操作,显示提示
-        if (!this.techPageParams.hasMore && !refresh && this.techList.length > 0) {
-          uni.showToast({
-            title: '已加载全部内容',
-            icon: 'none',
-            duration: 2000
-          });
+          techList.value = list || []
+        } else {
+          techList.value = [...techList.value, ...(list || [])]
         }
-      }
-    },
-    
-    // 获取政策解读数据
-    async fetchPolicyData(refresh = false) {
-      if (refresh) {
-        this.policyPageParams.page = 1;
-        this.policyPageParams.hasMore = true;
-      }
-      
-      // 如果没有更多数据则不请求
-      if (!this.policyPageParams.hasMore && !refresh) return;
-      
-      this.loading = true;
-      
-      try {
-        // 构建请求参数
-        const params = {
-          page: this.policyPageParams.page,
-          pageSize: this.policyPageParams.pageSize,
-          category: 'policy'
-        };
-        
-        // 发起请求
-        getKnowledgeList(params).then(response=>{
-			console.log("response农技333",response);
-			// 解析响应
-			if (response.data.data && response.data.code === 200) {
-			  const { list, total } = response.data.data;
-			  
-			  if (refresh) {
-			    this.policyList = list || [];
-			  } else {
-			    this.policyList = [...this.policyList, ...(list || [])];
-			  }
-			  console.log("this.policyList",this.policyList);
-			  // 更新分页状态
-			  this.policyPageParams.page++;
-			  this.policyPageParams.hasMore = this.policyList.length < total;
-			} else {
-			  uni.showToast({
-			    title: (response.data && response.data.message) || '获取数据失败',
-			    icon: 'none'
-			  });
-			}
-		}).catch(err => [err, null]);
-      } catch (error) {
-        console.error('获取政策解读数据失败:', error);
+        console.log("techList", techList.value)
+        // 更新分页状态
+        techPageParams.page++
+        techPageParams.hasMore = techList.value.length < total
+      } else {
         uni.showToast({
-          title: '网络异常,请稍后再试',
+          title: (response.data && response.data.message) || '获取数据失败',
           icon: 'none'
-        });
-      } finally {
-        this.loading = false;
-        if (refresh) {
-          this.isRefreshing = false;
-        }
-        
-        // 如果加载完成后没有更多数据,并且不是刷新操作,显示提示
-        if (!this.policyPageParams.hasMore && !refresh && this.policyList.length > 0) {
-          uni.showToast({
-            title: '已加载全部内容',
-            icon: 'none',
-            duration: 2000
-          });
-        }
+        })
       }
-    },
+    }).catch(err => [err, null])
+       
+  } catch (error) {
+    console.error('获取农技知识数据失败:', error)
+    uni.showToast({
+      title: '网络异常,请稍后再试',
+      icon: 'none'
+    })
+  } finally {
+    loading.value = false
+    if (refresh) {
+      isRefreshing.value = false
+    }
     
-    // 下拉刷新
-    async onRefresh() {
-      this.isRefreshing = true;
-      
-      if (this.currentTab === 0) {
-        // 刷新农技知识数据
-        await this.fetchTechKnowledge(true);
-      } else {
-        // 刷新政策解读数据
-        await this.fetchPolicyData(true);
-      }
-    },
+    // 如果加载完成后没有更多数据,并且不是刷新操作,显示提示
+    if (!techPageParams.hasMore && !refresh && techList.value.length > 0) {
+      uni.showToast({
+        title: '已加载全部内容',
+        icon: 'none',
+        duration: 2000
+      })
+    }
+  }
+}
+
+// 获取政策解读数据
+const fetchPolicyData = async (refresh = false) => {
+  if (refresh) {
+    policyPageParams.page = 1
+    policyPageParams.hasMore = true
+  }
+  
+  // 如果没有更多数据则不请求
+  if (!policyPageParams.hasMore && !refresh) return
+  
+  loading.value = true
+  
+  try {
+	// 构建请求参数
+    const params = {
+        pageNum: policyPageParams.page,
+        pageSize: policyPageParams.pageSize,
+        contentCategory: 'policy'
+    };
     
-    // 加载更多
-    loadMore() {
-      if (this.loading) return;
-      
-      console.log('Loading more content');
-      
-      if (this.currentTab === 0) {
-        // 检查是否还有更多农技知识数据
-        if (!this.techPageParams.hasMore) {
-          uni.showToast({
-            title: '已加载全部内容',
-            icon: 'none',
-            duration: 2000
-          });
-          return;
+    // 发起请求
+    getKnowledgeList(params).then(response => {
+      console.log("response农技333", response)
+      // 解析响应
+      if (response.data.data && response.data.code === 200) {
+        const { list, total } = response.data.data
+        
+        if (refresh) {
+          policyList.value = list || []
+        } else {
+          policyList.value = [...policyList.value, ...(list || [])]
         }
-        // 加载更多农技知识数据
-        this.fetchTechKnowledge();
+        console.log("policyList", policyList.value)
+        // 更新分页状态
+        policyPageParams.page++
+        policyPageParams.hasMore = policyList.value.length < total
       } else {
-        // 检查是否还有更多政策解读数据
-        if (!this.policyPageParams.hasMore) {
-          uni.showToast({
-            title: '已加载全部内容',
-            icon: 'none',
-            duration: 2000
-          });
-          return;
-        }
-        // 加载更多政策解读数据
-        this.fetchPolicyData();
+        uni.showToast({
+          title: (response.data && response.data.message) || '获取数据失败',
+          icon: 'none'
+        })
       }
-    },
-    
-    // 跳转AI问答
-    navigateToAI() {
-      uni.navigateTo({
-        url: '/pages/knowledge/ai-chat/index'
-      });
-    },
+    }).catch(err => [err, null])
+  } catch (error) {
+    console.error('获取政策解读数据失败:', error)
+    uni.showToast({
+      title: '网络异常,请稍后再试',
+      icon: 'none'
+    })
+  } finally {
+    loading.value = false
+    if (refresh) {
+      isRefreshing.value = false
+    }
     
-    // 处理文章阅读量更新
-    handleViewCountUpdate(data) {
-      if (!data || !data.id) return;
-      
-      // 更新技术文章列表中的阅读量
-      if (data.type === 'tech' || !data.type) {
-        const index = this.techList.findIndex(item => item.id === data.id);
-        if (index !== -1) {
-          this.techList[index].viewCount = data.viewCount;
-        }
-      }
-      
-      // 更新政策解读列表中的阅读量
-      if (data.type === 'policy') {
-        const index = this.policyList.findIndex(item => item.id === data.id);
-        if (index !== -1) {
-          this.policyList[index].viewCount = data.viewCount;
-        }
-      }
+    // 如果加载完成后没有更多数据,并且不是刷新操作,显示提示
+    if (!policyPageParams.hasMore && !refresh && policyList.value.length > 0) {
+      uni.showToast({
+        title: '已加载全部内容',
+        icon: 'none',
+        duration: 2000
+      })
     }
   }
 }
+
+// 下拉刷新
+const onRefresh = async () => {
+  isRefreshing.value = true
+  
+  if (currentTab.value === 0) {
+    // 刷新农技知识数据
+    await fetchTechKnowledge(true)
+  } else {
+    // 刷新政策解读数据
+    await fetchPolicyData(true)
+  }
+}
+
+// 加载更多
+const loadMore = () => {
+  if (loading.value) return
+  
+  console.log('Loading more content')
+  
+  if (currentTab.value === 0) {
+    // 检查是否还有更多农技知识数据
+    if (!techPageParams.hasMore) {
+      uni.showToast({
+        title: '已加载全部内容',
+        icon: 'none',
+        duration: 2000
+      })
+      return
+    }
+    // 加载更多农技知识数据
+    fetchTechKnowledge()
+  } else {
+    // 检查是否还有更多政策解读数据
+    if (!policyPageParams.hasMore) {
+      uni.showToast({
+        title: '已加载全部内容',
+        icon: 'none',
+        duration: 2000
+      })
+      return
+    }
+    // 加载更多政策解读数据
+    fetchPolicyData()
+  }
+}
+
+// 跳转AI问答
+const navigateToAI = () => {
+  uni.navigateTo({
+    url: '/pages/knowledge/ai-chat/index'
+  })
+}
+
+// 处理文章阅读量更新
+const handleViewCountUpdate = (data) => {
+  if (!data || !data.id) return
+  
+  // 更新技术文章列表中的阅读量
+  if (data.type === 'tech' || !data.type) {
+    const index = techList.value.findIndex(item => item.id === data.id)
+    if (index !== -1) {
+      techList.value[index].viewCount = data.viewCount
+    }
+  }
+  
+  // 更新政策解读列表中的阅读量
+  if (data.type === 'policy') {
+    const index = policyList.value.findIndex(item => item.id === data.id)
+    if (index !== -1) {
+      policyList.value[index].viewCount = data.viewCount
+    }
+  }
+}
+
+// 页面加载完成
+onMounted(() => {
+  console.log('Page mounted, current tab:', currentTab.value)
+  // 初始加载数据
+  fetchTechKnowledge()
+  
+  // 监听文章阅读量更新事件
+  uni.$on('updateArticleViewCount', handleViewCountUpdate)
+})
+
+// 页面显示时触发
+onShow(() => {
+  // 获取当前页面路由信息,判断是否是从详情页返回
+  const pages = getCurrentPages()
+  if (pages.length > 1) {
+    const prePage = pages[pages.length - 2]
+    // 如果前一个页面是详情页,检查是否需要刷新数据
+    if (prePage && prePage.route && prePage.route.includes('knowledge/detail')) {
+      console.log('从详情页返回,更新阅读量显示')
+      // 由于已经通过事件机制更新了列表,这里只需要触发一下视图更新
+      if (currentTab.value === 0) {
+        techList.value = [...techList.value]
+      } else {
+        policyList.value = [...policyList.value]
+      }
+    }
+  }
+})
+
+// 页面销毁前
+onBeforeUnmount(() => {
+  // 移除事件监听
+  uni.$off('updateArticleViewCount', handleViewCountUpdate)
+})
 </script>
 
 <style scoped>

+ 224 - 188
pages/login/forget-password.vue

@@ -95,212 +95,216 @@
   </view>
 </template>
 
-<script>
-import storage from "@/utils/storage.js";
-// import { resetPassword, sendVerificationCode } from '@/api/services/auth.js';
-
-export default {
-  data() {
-    return {
-      formData: {
-        phone: '',
-        code: '',
-        password: '',
-        confirmPassword: ''
-      },
-      showPassword: false,
-      showConfirmPassword: false,
-      codeBtnText: '获取验证码',
-      codeBtnDisabled: false,
-      countdown: 60,
-      isSubmitting: false
+<script setup>
+import { ref, reactive } from 'vue'
+import storage from "@/utils/storage.js"
+// import { resetPassword, sendVerificationCode } from '@/api/services/auth.js'
+
+// 响应式数据
+const formData = reactive({
+  phone: '',
+  code: '',
+  password: '',
+  confirmPassword: ''
+})
+
+const showPassword = ref(false)
+const showConfirmPassword = ref(false)
+const codeBtnText = ref('获取验证码')
+const codeBtnDisabled = ref(false)
+const countdown = ref(60)
+const isSubmitting = ref(false)
+
+// 方法定义
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const togglePasswordVisibility = () => {
+  showPassword.value = !showPassword.value
+}
+
+const toggleConfirmPasswordVisibility = () => {
+  showConfirmPassword.value = !showConfirmPassword.value
+}
+
+const navigateToLogin = () => {
+  uni.navigateBack()
+}
+
+const startCountdown = () => {
+  codeBtnDisabled.value = true
+  codeBtnText.value = `${countdown.value}秒`
+  
+  const timer = setInterval(() => {
+    countdown.value--
+    codeBtnText.value = `${countdown.value}秒`
+    
+    if (countdown.value <= 0) {
+      clearInterval(timer)
+      codeBtnDisabled.value = false
+      codeBtnText.value = '获取验证码'
+      countdown.value = 60
     }
-  },
-  methods: {
-    goBack() {
-      uni.navigateBack()
-    },
-    togglePasswordVisibility() {
-      this.showPassword = !this.showPassword
-    },
-    toggleConfirmPasswordVisibility() {
-      this.showConfirmPassword = !this.showConfirmPassword
-    },
-    navigateToLogin() {
-      uni.navigateBack()
+  }, 1000)
+}
+
+const getVerificationCode = () => {
+  if (codeBtnDisabled.value) return
+  
+  if (!formData.phone) {
+    uni.showToast({
+      title: '请输入手机号',
+      icon: 'none'
+    })
+    return
+  }
+  
+  if (!/^1\d{10}$/.test(formData.phone)) {
+    uni.showToast({
+      title: '请输入正确的手机号',
+      icon: 'none'
+    })
+    return
+  }
+  
+  // 发送验证码请求
+  uni.showLoading({ title: '发送中...' })
+  
+  // 调用发送验证码接口
+  uni.request({
+    url: 'API_URL/user/sendCode', // 替换为实际接口地址
+    method: 'POST',
+    data: {
+      phone: formData.phone,
+      type: 'reset'
     },
-    getVerificationCode() {
-      if (this.codeBtnDisabled) return
-      
-      if (!this.formData.phone) {
+    success: (res) => {
+      if (res.data.code === 200) {
         uni.showToast({
-          title: '请输入手机号',
-          icon: 'none'
+          title: '验证码已发送',
+          icon: 'success'
         })
-        return
-      }
-      
-      if (!/^1\d{10}$/.test(this.formData.phone)) {
+        startCountdown()
+      } else {
         uni.showToast({
-          title: '请输入正确的手机号',
+          title: res.data.msg || '发送失败,请稍后重试',
           icon: 'none'
         })
-        return
       }
-      
-      // 发送验证码请求
-      uni.showLoading({ title: '发送中...' })
-      
-      // 调用发送验证码接口
-      uni.request({
-        url: 'API_URL/user/sendCode', // 替换为实际接口地址
-        method: 'POST',
-        data: {
-          phone: this.formData.phone,
-          type: 'reset'
-        },
-        success: (res) => {
-          if (res.data.code === 200) {
-            uni.showToast({
-              title: '验证码已发送',
-              icon: 'success'
-            });
-            this.startCountdown();
-          } else {
-            uni.showToast({
-              title: res.data.msg || '发送失败,请稍后重试',
-              icon: 'none'
-            });
-          }
-        },
-        fail: (err) => {
-          uni.showToast({
-            title: '网络异常,请稍后重试',
-            icon: 'none'
-          });
-          console.error(err);
-        },
-        complete: () => {
-          uni.hideLoading();
-        }
-      });
     },
-    startCountdown() {
-      this.codeBtnDisabled = true
-      this.codeBtnText = `${this.countdown}秒`
-      
-      const timer = setInterval(() => {
-        this.countdown--
-        this.codeBtnText = `${this.countdown}秒`
-        
-        if (this.countdown <= 0) {
-          clearInterval(timer)
-          this.codeBtnDisabled = false
-          this.codeBtnText = '获取验证码'
-          this.countdown = 60
-        }
-      }, 1000)
+    fail: (err) => {
+      uni.showToast({
+        title: '网络异常,请稍后重试',
+        icon: 'none'
+      })
+      console.error(err)
     },
-    validateForm() {
-      if (!this.formData.phone) {
-        uni.showToast({
-          title: '请输入手机号',
-          icon: 'none'
-        })
-        return false
-      }
-      
-      if (!/^1\d{10}$/.test(this.formData.phone)) {
-        uni.showToast({
-          title: '请输入正确的手机号',
-          icon: 'none'
-        })
-        return false
-      }
-      
-      if (!this.formData.code) {
-        uni.showToast({
-          title: '请输入验证码',
-          icon: 'none'
-        })
-        return false
-      }
-      
-      if (!this.formData.password) {
-        uni.showToast({
-          title: '请设置新密码',
-          icon: 'none'
-        })
-        return false
-      }
-      
-      if (this.formData.password.length < 6) {
+    complete: () => {
+      uni.hideLoading()
+    }
+  })
+}
+
+const validateForm = () => {
+  if (!formData.phone) {
+    uni.showToast({
+      title: '请输入手机号',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (!/^1\d{10}$/.test(formData.phone)) {
+    uni.showToast({
+      title: '请输入正确的手机号',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (!formData.code) {
+    uni.showToast({
+      title: '请输入验证码',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (!formData.password) {
+    uni.showToast({
+      title: '请设置新密码',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (formData.password.length < 6) {
+    uni.showToast({
+      title: '密码长度不能少于6位',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (formData.password !== formData.confirmPassword) {
+    uni.showToast({
+      title: '两次输入的密码不一致',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  return true
+}
+
+const handleResetPassword = () => {
+  if (!validateForm()) return
+  
+  isSubmitting.value = true
+  
+  // 调用重置密码接口
+  uni.showLoading({ title: '提交中...' })
+  
+  // 调用重置密码接口
+  uni.request({
+    url: 'API_URL/user/resetPassword', // 替换为实际接口地址
+    method: 'POST',
+    data: {
+      phone: formData.phone,
+      code: formData.code,
+      password: formData.password
+    },
+    success: (res) => {
+      if (res.data.code === 200) {
         uni.showToast({
-          title: '密码长度不能少于6位',
-          icon: 'none'
+          title: '密码重置成功',
+          icon: 'success'
         })
-        return false
-      }
-      
-      if (this.formData.password !== this.formData.confirmPassword) {
+        
+        // 延迟跳转到登录页
+        setTimeout(() => {
+          uni.navigateBack()
+        }, 1500)
+      } else {
         uni.showToast({
-          title: '两次输入的密码不一致',
+          title: res.data.msg || '重置失败,请稍后重试',
           icon: 'none'
         })
-        return false
       }
-      
-      return true
     },
-    handleResetPassword() {
-      if (!this.validateForm()) return
-      
-      this.isSubmitting = true;
-      
-      // 调用重置密码接口
-      uni.showLoading({ title: '提交中...' })
-      
-      // 调用重置密码接口
-      uni.request({
-        url: 'API_URL/user/resetPassword', // 替换为实际接口地址
-        method: 'POST',
-        data: {
-          phone: this.formData.phone,
-          code: this.formData.code,
-          password: this.formData.password
-        },
-        success: (res) => {
-          if (res.data.code === 200) {
-            uni.showToast({
-              title: '密码重置成功',
-              icon: 'success'
-            });
-            
-            // 延迟跳转到登录页
-            setTimeout(() => {
-              uni.navigateBack();
-            }, 1500);
-          } else {
-            uni.showToast({
-              title: res.data.msg || '重置失败,请稍后重试',
-              icon: 'none'
-            });
-          }
-        },
-        fail: (err) => {
-          uni.showToast({
-            title: '网络异常,请稍后重试',
-            icon: 'none'
-          });
-          console.error(err);
-        },
-        complete: () => {
-          uni.hideLoading();
-          this.isSubmitting = false;
-        }
-      });
+    fail: (err) => {
+      uni.showToast({
+        title: '网络异常,请稍后重试',
+        icon: 'none'
+      })
+      console.error(err)
+    },
+    complete: () => {
+      uni.hideLoading()
+      isSubmitting.value = false
     }
-  }
+  })
 }
 </script>
 
@@ -491,4 +495,36 @@ export default {
   }
 }
 /* #endif */
+
+/* #ifdef APP-PLUS || MP-HARMONY */
+/* APP和鸿蒙特殊样式调整 */
+.forget-container {
+  padding-top: 20rpx;
+}
+
+.input-group {
+  background-color: #f5f5f5;
+  border-radius: 8rpx;
+  padding: 20rpx 15rpx;
+  border-bottom: none;
+  margin-bottom: 20rpx;
+}
+
+.input {
+  background-color: transparent;
+}
+
+.submit-btn {
+  margin-top: 40rpx;
+}
+
+.notice {
+  margin-top: 40rpx;
+  border: 1rpx solid #E0E0E0;
+}
+
+.wechat-tip {
+  margin-top: 40rpx;
+}
+/* #endif */
 </style> 

+ 222 - 237
pages/login/index.vue

@@ -14,7 +14,7 @@
 		</view>
 
 		<view class="login-section">
-			<!-- #ifdef H5 -->
+			<!-- #ifndef MP-WEIXIN -->
 			<view class="form-container">
 				<view class="input-group">
 					<input type="text" v-model="phoneNumber" placeholder="请输入手机号" class="input-field" maxlength="11" />
@@ -22,9 +22,9 @@
 				<view class="input-group">
 					<input type="password" v-model="password" placeholder="请输入密码" class="input-field" />
 				</view>
-				<!-- <view class="form-actions">
+				<view class="form-actions">
 					<text class="forgot-password" @click="navigateToForgetPassword">忘记密码?</text>
-				</view> -->
+				</view>
 				<button type="primary" class="login-btn" @click="handlePhoneLogin">登录</button>
 				<view class="register-link">
 					还没有账号?<text class="link" @click="navigateToRegister">立即注册</text>
@@ -38,24 +38,6 @@
 				<text>一键登录</text>
 			</button>
 			<!-- #endif -->
-
-			<!-- #ifdef APP-PLUS -->
-			<view class="form-container">
-				<view class="input-group">
-					<input type="text" v-model="phoneNumber" placeholder="请输入手机号" class="input-field" />
-				</view>
-				<view class="input-group">
-					<input type="password" v-model="password" placeholder="请输入密码" class="input-field" />
-				</view>
-				<view class="form-actions">
-					<text class="forgot-password" @click="navigateToForgetPassword">忘记密码?</text>
-				</view>
-				<button type="primary" class="login-btn" @click="handlePhoneLogin">登录</button>
-				<view class="register-link">
-					还没有账号?<text class="link" @click="navigateToRegister">立即注册</text>
-				</view>
-			</view>
-			<!-- #endif -->
 		</view>
 
 		<view class="privacy-agreement">
@@ -70,246 +52,235 @@
 	</view>
 </template>
 
-<script>
+<script setup>
+	import { ref, onMounted } from 'vue'
 	import {
 		mpAutoLogin, phoneLogin
 	} from "@/api/services/connect.js";
 	import storage from "@/utils/storage.js";
 	import Foundation from "@/utils/Foundation.js";
-	export default {
-		data() {
-			return {
-				// 以下是登录页面测试功能 结束
-				agreed: false,
-				// 是否展示手机号码授权弹窗,默认第一步不展示,要先获取用户基础信息
-				phoneAuthPopup: false,
-				// 授权信息展示,商城名称
-				// projectName: config.name,
-				//微信返回信息,用于揭秘信息,获取sessionkey
-				code: "",
-				//微信昵称
-				nickName: "",
-				//微信头像
-				avatarUrl: "",
-				gender: "",
-				// H5登录信息
-				phoneNumber: "",
-				password: "",
-				loading: false
-			}
-		},
-		onLoad() {
-			// // 检查是否已登录
-			// if (isLoggedIn()) {
-			// 	this.redirectToHome();
-			// }
-
-			// #ifdef MP-WEIXIN
-			//获取code
-			uni.login({
-				success: (res) => {
-					if (res.errMsg === "login:ok") {
-						this.code = res.code
-					} else {
-						uni.showToast({
-							title: "系统异常,请联系管理员!"
-						})
-					}
-				},
-			});
-			// #endif
-		},
-		methods: {
-			goBack() {
-				uni.navigateBack();
-			},
-			toggleAgreement() {
-				this.agreed = !this.agreed;
-				console.log("this.agreed", this.agreed);
 
+	// Reactive data
+	const agreed = ref(false)
+	const code = ref("")
+	const nickName = ref("")
+	const avatarUrl = ref("")
+	const gender = ref("")
+	const phoneNumber = ref("")
+	const password = ref("")
+	const loading = ref(false)
+	const logingFlag = ref(false)
+	const isLogin = ref(0)
+
+	// Lifecycle hooks - uni-app specific lifecycle
+	onMounted(() => {
+		// // 检查是否已登录
+		// if (isLoggedIn()) {
+		// 	redirectToHome();
+		// }
+
+		// #ifdef MP-WEIXIN
+		//获取code
+		uni.login({
+			success: (res) => {
+				if (res.errMsg === "login:ok") {
+					code.value = res.code
+				} else {
+					uni.showToast({
+						title: "系统异常,请联系管理员!"
+					})
+				}
 			},
-			navigateToTerms() {
-				uni.navigateTo({
-					url: '/pages/privacy/terms'
-				});
-			},
-			navigateToPrivacy() {
-				uni.navigateTo({
-					url: '/pages/privacy/index'
-				});
-			},
-			navigateToForgetPassword() {
-				uni.navigateTo({
-					url: '/pages/login/forget-password'
-				});
-			},
-			navigateToRegister() {
-				uni.navigateTo({
-					url: '/pages/login/register'
+		});
+		// #endif
+	})
+
+	// Methods
+	const goBack = () => {
+		uni.navigateBack();
+	}
+
+	const toggleAgreement = () => {
+		agreed.value = !agreed.value;
+		console.log("agreed", agreed.value);
+	}
+
+	const navigateToTerms = () => {
+		uni.navigateTo({
+			url: '/pages/privacy/terms'
+		});
+	}
+
+	const navigateToPrivacy = () => {
+		uni.navigateTo({
+			url: '/pages/privacy/index'
+		});
+	}
+
+	const navigateToForgetPassword = () => {
+		uni.navigateTo({
+			url: '/pages/login/forget-password'
+		});
+	}
+
+	const navigateToRegister = () => {
+		uni.navigateTo({
+			url: '/pages/login/register'
+		});
+	}
+
+	// H5平台手机号密码登录方法
+	const handlePhoneLogin = () => {
+		if (!agreed.value) {
+			uni.showToast({
+				title: "请同意用户协议和隐私政策",
+				icon: 'none'
+			})
+			return
+		}
+
+		if (!phoneNumber.value || !password.value) {
+			uni.showToast({
+				title: "请输入手机号和密码",
+				icon: 'none'
+			})
+			return
+		}
+
+		// 手机号验证
+		const phone = phoneNumber.value; // 获取用户输入的手机号
+		const result = Foundation.validatePhoneNumber(phone);
+
+		if (!result.valid) {
+			uni.showToast({
+				title: result.message,
+				icon: 'none'
+			});
+			return
+		}
+
+		loading.value = true;
+		uni.showLoading({ title: '登录中...' })
+		const data = {
+			username: phoneNumber.value,
+			password: password.value,
+			phoneNumber: phoneNumber.value,
+		}
+		phoneLogin(data).then(res => {
+			console.log("res登录", res);
+			if (res.data.code === 200) {
+				// 登录成功
+				storage.setAccessToken(res.data.data.access_token);
+				storage.setUserInfo(res.data.data.userInfo);
+				storage.setHasLogin(true);
+
+				uni.showToast({
+					title: "登录成功!",
+					icon: "success",
 				});
-			},
-			showAgreementWarning() {
+
+				// 登录成功后返回或跳转到首页
+				setTimeout(() => {
+					uni.navigateBack({
+						delta: 1,
+					});
+				}, 1500);
+			} else {
+				// 登录失败
 				uni.showToast({
-					title: '请同意用户协议和隐私政策',
+					title: res.data.msg || "登录失败,请检查账号密码",
 					icon: 'none'
 				});
-			},
-			// H5平台手机号密码登录方法
-			handlePhoneLogin() {
-				if (!this.agreed) {
-					uni.showToast({
-						title: "请同意用户协议和隐私政策",
-						icon: 'none'
-					})
-					return
-				}
+			}
+		})
+		.catch(err => {
+			uni.showToast({
+				title: "网络异常,请稍后重试",
+				icon: 'none'
+			});
+			console.error(err);
+		})
+		.finally(() => {
+			uni.hideLoading();
+			loading.value = false;
+		});
+	}
 
-				if (!this.phoneNumber || !this.password) {
-					uni.showToast({
-						title: "请输入手机号和密码",
-						icon: 'none'
-					})
-					return
-				}
+	// 微信小程序登录方法
+	const getUserProfile = () => {
+		if (!agreed.value) {
+			uni.showToast({
+				title: "请同意用户协议和隐私政策",
+				icon: 'none'
+			})
+			return
+		}
+		logingFlag.value = true;
 
-				// 手机号验证
-				const phone = this.phoneNumber; // 获取用户输入的手机号
-      	const result = Foundation.validatePhoneNumber(phone);
+		// #ifdef MP-WEIXIN
+		if (code.value) {
+			// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认
+			uni.getUserProfile({
+				desc: "获取你的昵称、头像、地区及性别", // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
+				success: (res) => {
+					console.log("success", res)
+					nickName.value = res.userInfo.nickName;
+					avatarUrl.value = res.userInfo.avatarUrl;
+					gender.value = res.userInfo.gender;
+
+					let iv = res.iv;
+					let encryptedData = res.encryptedData;
+
+					let codeVal = code.value;
+					let avatarUrlVal = avatarUrl.value;
+					let nickNameVal = nickName.value;
+					let genderVal = gender.value;
+					isLogin.value = 2
+					mpAutoLogin({
+						encryptedData,
+						iv,
+						code: codeVal,
+						avatarUrl: avatarUrlVal,
+						nickName: nickNameVal,
+						gender: genderVal,
+					}).then((apiRes) => {
+						console.log("apiRes", apiRes);
+						storage.setAccessToken(apiRes.data.data.token);
+						// storage.setRefreshToken(apiRes.data.result.refreshToken);
 
-				if (!result.valid) {
 						uni.showToast({
-								title: result.message,
-								icon: 'none'
+							title: "登录成功!",
+							icon: "none",
 						});
-						return
-				}
-
-				this.loading = true;
-				uni.showLoading({ title: '登录中...' })
-				const data = {
-					username: this.phoneNumber,
-					password: this.password,
-					phoneNumber:this.phoneNumber,
-				}
-				phoneLogin(data).then(res => {
-						console.log("res登录", res);
-						if (res.data.code === 200) {
-							// 登录成功
-							storage.setAccessToken(res.data.data.access_token);
-							storage.setUserInfo(res.data.data.userInfo);
-							storage.setHasLogin(true);
-
-							uni.showToast({
-								title: "登录成功!",
-								icon: "success",
+						//存储用户信息
+						storage.setUserInfo(apiRes.data.data.userInfo);
+						storage.setHasLogin(true);
+						// 用户手动授权头像昵称
+						if (apiRes.data.data.isNewUser) {
+							// 新用户跳转上传头像昵称界面
+							uni.navigateTo({
+								url: `/pages/userInfo/index?openId=${apiRes.data.data.userInfo.openId}`
 							});
-
-							// 登录成功后返回或跳转到首页
-							setTimeout(() => {
-								uni.navigateBack({
-									delta: 1,
-								});
-							}, 1500);
 						} else {
-							// 登录失败
-							uni.showToast({
-								title: res.data.msg || "登录失败,请检查账号密码",
-								icon: 'none'
+							// 老用户直接跳转首页
+							// wx.switchTab({
+							// 	url: '/pages/user/index'
+							// });
+							uni.navigateBack({
+								delta: 1,
 							});
 						}
-					})
-					.catch(err => {
-						uni.showToast({
-							title: "网络异常,请稍后重试",
-							icon: 'none'
-						});
-						console.error(err);
-					})
-					.finally(() => {
-						uni.hideLoading();
-						this.loading = false;
 					});
-			},
-			// 微信小程序登录方法
-			getUserProfile(e) {
-				if (!this.agreed) {
-					uni.showToast({
-						title: "请同意用户协议和隐私政策",
-						icon: 'none'
-					})
-					return
-				}
-				this.logingFlag = true;
-
-				// #ifdef MP-WEIXIN
-				if (this.code) {
-					// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认
-					uni.getUserProfile({
-						desc: "获取你的昵称、头像、地区及性别", // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
-						success: (res) => {
-							console.log("success", res)
-							this.nickName = res.userInfo.nickName;
-							this.avatarUrl = res.userInfo.avatarUrl;
-							this.gender = res.userInfo.gender;
-
-							let iv = res.iv;
-							let encryptedData = res.encryptedData;
-
-							let code = this.code;
-							let avatarUrl = this.avatarUrl;
-							let nickName = this.nickName;
-							let gender = this.gender;
-							this.isLogin = 2
-							mpAutoLogin({
-								encryptedData,
-								iv,
-								code,
-								avatarUrl,
-								nickName,
-								gender,
-							}).then((apiRes) => {
-								console.log("apiRes", apiRes);
-								storage.setAccessToken(apiRes.data.data.token);
-								// storage.setRefreshToken(apiRes.data.result.refreshToken);
-
-								uni.showToast({
-									title: "登录成功!",
-									icon: "none",
-								});
-								//存储用户信息
-								storage.setUserInfo(apiRes.data.data.userInfo);
-								storage.setHasLogin(true);
-								// 用户手动授权头像昵称
-								if (apiRes.data.data.isNewUser) {
-									// 新用户跳转上传头像昵称界面
-									uni.navigateTo({
-										url: `/pages/userInfo/index?openId=${apiRes.data.data.userInfo.openId}`
-									});
-								} else {
-									// 老用户直接跳转首页
-									// wx.switchTab({
-									// 	url: '/pages/user/index'
-									// });
-									uni.navigateBack({
-										delta: 1,
-									});
-								}
-							});
 
-						},
-						fail: (res) => {
-							console.log("fail", res)
-						},
-					});
+				},
+				fail: (res) => {
+					console.log("fail", res)
+				},
+			});
 
-					this.logingFlag = false;
-				}
-				// #endif
-			},
-			redirectToHome() {
-				uni.navigateBack();
-			}
+			logingFlag.value = false;
 		}
+		// #endif
 	}
 </script>
 
@@ -499,6 +470,20 @@
 			margin: 0 auto;
 		}
 	}
+	/* #endif */
+
+	/* #ifdef APP-PLUS || MP-HARMONY */
+	/* APP和鸿蒙特殊样式调整 */
+	.login-container {
+		padding-top: 20rpx;
+	}
 
+	.input-field {
+		border: 1rpx solid #E0E0E0;
+	}
+
+	.login-btn {
+		margin-top: 20rpx;
+	}
 	/* #endif */
 </style>

+ 203 - 170
pages/login/register.vue

@@ -112,183 +112,189 @@
   </view>
 </template>
 
-<script>
-import storage from "@/utils/storage.js";
-import { register } from '@/api/services/connect.js';
-import Foundation from "@/utils/Foundation.js";
-export default {
-  data() {
-    return {
-      formData: {
-        phone: '',
-        code: '',
-        password: '',
-        confirmPassword: '',
-        nickname: ''
-      },
-      showPassword: false,
-      showConfirmPassword: false,
-      agreed: true,
-      codeBtnText: '获取验证码',
-      codeBtnDisabled: false,
-      countdown: 60,
-      isSubmitting: false
+<script setup>
+import { ref, reactive } from 'vue'
+import storage from "@/utils/storage.js"
+import { register } from '@/api/services/connect.js'
+import Foundation from "@/utils/Foundation.js"
+
+// 响应式数据
+const formData = reactive({
+  phone: '',
+  code: '',
+  password: '',
+  confirmPassword: '',
+  nickname: ''
+})
+
+const showPassword = ref(false)
+const showConfirmPassword = ref(false)
+const agreed = ref(true)
+const codeBtnText = ref('获取验证码')
+const codeBtnDisabled = ref(false)
+const countdown = ref(60)
+const isSubmitting = ref(false)
+
+// 方法定义
+const goBack = () => {
+  uni.navigateBack()
+}
+
+const togglePasswordVisibility = () => {
+  showPassword.value = !showPassword.value
+}
+
+const toggleConfirmPasswordVisibility = () => {
+  showConfirmPassword.value = !showConfirmPassword.value
+}
+
+const toggleAgreement = () => {
+  agreed.value = !agreed.value
+}
+
+const navigateToLogin = () => {
+  uni.navigateBack()
+}
+
+const navigateToTerms = () => {
+  uni.navigateTo({
+    url: '/pages/privacy/terms'
+  })
+}
+
+const navigateToPrivacy = () => {
+  uni.navigateTo({
+    url: '/pages/privacy/index'
+  })
+}
+
+const startCountdown = () => {
+  codeBtnDisabled.value = true
+  codeBtnText.value = `${countdown.value}秒`
+  
+  const timer = setInterval(() => {
+    countdown.value--
+    codeBtnText.value = `${countdown.value}秒`
+    
+    if (countdown.value <= 0) {
+      clearInterval(timer)
+      codeBtnDisabled.value = false
+      codeBtnText.value = '获取验证码'
+      countdown.value = 60
     }
-  },
-  methods: {
-    goBack() {
-      uni.navigateBack()
-    },
-    togglePasswordVisibility() {
-      this.showPassword = !this.showPassword
-    },
-    toggleConfirmPasswordVisibility() {
-      this.showConfirmPassword = !this.showConfirmPassword
-    },
-    toggleAgreement() {
-      this.agreed = !this.agreed
-    },
-    navigateToLogin() {
-      uni.navigateBack()
-    },
-    navigateToTerms() {
-      uni.navigateTo({
-        url: '/pages/privacy/terms'
-      })
-    },
-    navigateToPrivacy() {
-      uni.navigateTo({
-        url: '/pages/privacy/index'
-      })
-    },
-    startCountdown() {
-      this.codeBtnDisabled = true
-      this.codeBtnText = `${this.countdown}秒`
-      
-      const timer = setInterval(() => {
-        this.countdown--
-        this.codeBtnText = `${this.countdown}秒`
-        
-        if (this.countdown <= 0) {
-          clearInterval(timer)
-          this.codeBtnDisabled = false
-          this.codeBtnText = '获取验证码'
-          this.countdown = 60
-        }
-      }, 1000)
-    },
-    validateForm() {
-      if (!this.formData.phone) {
-        uni.showToast({
-          title: '请输入手机号',
-          icon: 'none'
-        })
-        return false
-      }
-      
-      const phone = this.formData.phone; // 获取用户输入的手机号
-      const result = Foundation.validatePhoneNumber(phone);
-
-      if (!result.valid) {
-          uni.showToast({
-              title: result.message,
-              icon: 'none'
-          });
-          return false;
-      }
-      
-      
-      if (!this.formData.password) {
-        uni.showToast({
-          title: '请设置密码',
-          icon: 'none'
-        })
-        return false
-      }
-      
-      if (this.formData.password.length < 6) {
-        uni.showToast({
-          title: '密码长度不能少于6位',
-          icon: 'none'
-        })
-        return false
-      }
-      
-      if (this.formData.password !== this.formData.confirmPassword) {
+  }, 1000)
+}
+
+const validateForm = () => {
+  if (!formData.phone) {
+    uni.showToast({
+      title: '请输入手机号',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  const phone = formData.phone // 获取用户输入的手机号
+  const result = Foundation.validatePhoneNumber(phone)
+
+  if (!result.valid) {
+    uni.showToast({
+      title: result.message,
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (!formData.password) {
+    uni.showToast({
+      title: '请设置密码',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (formData.password.length < 6) {
+    uni.showToast({
+      title: '密码长度不能少于6位',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  if (formData.password !== formData.confirmPassword) {
+    uni.showToast({
+      title: '两次输入的密码不一致',
+      icon: 'none'
+    })
+    return false
+  }
+  
+  return true
+}
+
+const handleRegister = () => {
+  if (!validateForm()) return
+  
+  if (!agreed.value) {
+    uni.showToast({
+      title: '请同意用户协议和隐私政策',
+      icon: 'none'
+    })
+    return
+  }
+  
+  isSubmitting.value = true
+  
+  // 调用注册接口
+  uni.showLoading({ title: '注册中...' })
+  
+  const data = {
+    phoneNumber: formData.phone,
+    password: formData.password,
+    username: formData.nickname || `用户${formData.phone.substr(-4)}`
+  }
+  
+  // 调用注册接口
+  register(data)
+    .then(res => {
+      console.log("res注册", res)
+      if (res.data.code === 200) {
         uni.showToast({
-          title: '两次输入的密码不一致',
-          icon: 'none'
+          title: '注册成功',
+          icon: 'success'
         })
-        return false
-      }
-      
-      return true
-    },
-    handleRegister() {
-      if (!this.validateForm()) return
-      
-      if (!this.agreed) {
+        storage.setUserInfo(data)
+        // 自动登录
+        // if (res.data.data && res.data.data.token) {
+        //   storage.setAccessToken(res.data.data.token)
+        //   storage.setUserInfo(data)
+        //   storage.setHasLogin(true)
+        // }
+        
+        // 延迟跳转到登录页或首页
+        setTimeout(() => {
+          uni.navigateBack({
+            delta: 1,
+          })
+        }, 1500)
+      } else {
         uni.showToast({
-          title: '请同意用户协议和隐私政策',
+          title: res.data.msg || '注册失败,请稍后重试',
           icon: 'none'
         })
-        return
       }
-      
-      this.isSubmitting = true;
-      
-      // 调用注册接口
-      uni.showLoading({ title: '注册中...' })
-	  
-      const data = {
-          phoneNumber: this.formData.phone,
-          password: this.formData.password,
-          username: this.formData.nickname || `用户${this.formData.phone.substr(-4)}`
-        }
-		
-      // 调用注册接口
-	  register(data)
-	    .then(res => {
-			console.log("res注册",res);
-	      if (res.data.code === 200) {
-	        uni.showToast({
-	          title: '注册成功',
-	          icon: 'success'
-	        });
-	        storage.setUserInfo(data);
-	        // 自动登录
-	        // if (res.data.data && res.data.data.token) {
-	        //   storage.setAccessToken(res.data.data.token);
-	        //   storage.setUserInfo(data);
-	        //   storage.setHasLogin(true);
-	        // }
-	        
-	        // 延迟跳转到登录页或首页
-	        setTimeout(() => {
-	          uni.navigateBack({
-	            delta: 1,
-	          });
-	        }, 1500);
-	      } else {
-	        uni.showToast({
-	          title: res.data.msg || '注册失败,请稍后重试',
-	          icon: 'none'
-	        });
-	      }
-	    })
-	    .catch(err => {
-	      uni.showToast({
-	        title: '网络异常,请稍后重试',
-	        icon: 'none'
-	      });
-	      console.error(err);
-	    })
-	    .finally(() => {
-	      uni.hideLoading();
-	      this.isSubmitting = false;
-	    });
-    }
-  }
+    })
+    .catch(err => {
+      uni.showToast({
+        title: '网络异常,请稍后重试',
+        icon: 'none'
+      })
+      console.error(err)
+    })
+    .finally(() => {
+      uni.hideLoading()
+      isSubmitting.value = false
+    })
 }
 </script>
 
@@ -465,4 +471,31 @@ export default {
   }
 }
 /* #endif */
+
+/* #ifdef APP-PLUS || MP-HARMONY */
+/* APP和鸿蒙特殊样式调整 */
+.register-container {
+  padding-top: 20rpx;
+}
+
+.input-group {
+  background-color: #f5f5f5;
+  border-radius: 8rpx;
+  padding: 20rpx 15rpx;
+  border-bottom: none;
+  margin-bottom: 20rpx;
+}
+
+.input {
+  background-color: transparent;
+}
+
+.register-btn {
+  margin-top: 40rpx;
+}
+
+.privacy-agreement {
+  margin-top: auto;
+}
+/* #endif */
 </style> 

+ 331 - 383
pages/plots/list.vue

@@ -181,404 +181,352 @@
 	</view>
 </template>
 
-<script>
-	import {
-		fetchUserFieldList,
-		getUserCurrentField,
-		searchUserField
-	} from '@/api/services/field.js';
-	import storage from "@/utils/storage.js";
-	export default {
-		// 配置页面的下拉刷新
-		onPullDownRefresh: true,
-
-		data() {
-			return {
-				isLoading: true,
-				searchKeyword: '',
-				currentBlock: {
-					id: '',
-					code: '',
-					name: '',
-					farmName: '',
-					manager: '',
-					area: 0,
-					type: '',
-					crop: '',
-					status: 'active',
-					deviceCount: 0,
-					onlineDevices: 0,
-					alerts: 0
-				},
-				blocks: [],
-				searchTimer: null,
-				// 添加分页相关数据
-				pageNum: 1, // 当前页码
-				pageSize: 10, // 每页数量
-				totalCount: 0, // 总记录数
-				loadingMore: false, // 是否正在加载更多
-				hasMore: true, // 是否还有更多数据
-				isLoadingTip: false, // 是否显示底部加载提示
-				refreshing: false // 是否正在下拉刷新
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import {
+	fetchUserFieldList,
+	searchUserField
+} from '@/api/services/field.js'
+import storage from "@/utils/storage.js"
+
+// 响应式数据
+const isLoading = ref(true)
+const searchKeyword = ref('')
+const currentBlock = reactive({
+	id: '',
+	code: '',
+	name: '',
+	farmName: '',
+	manager: '',
+	area: 0,
+	type: '',
+	crop: '',
+	status: 'active',
+	deviceCount: 0,
+	onlineDevices: 0,
+	alerts: 0
+})
+const blocks = ref([])
+const searchTimer = ref(null)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const totalCount = ref(0)
+const loadingMore = ref(false)
+const hasMore = ref(true)
+const isLoadingTip = ref(false)
+const refreshing = ref(false)
+
+// 计算属性
+const filteredBlocks = computed(() => {
+	return blocks.value
+})
+
+// 获取当前选中的地块
+const fetchCurrentField = () => {
+	try {
+		getUserCurrentField().then((res) => {
+			console.log("fetchCurrentField", res)
+			if (res.data && res.data.code === 200 && res.data.data) {
+				Object.assign(currentBlock, res.data.data)
+			} else {
+				console.log('未查询到当前地块,将使用列表第一个地块')
 			}
-		},
+		}).catch(err => {
+			console.error('获取当前地块失败', err)
+		})
+	} catch (e) {
+		console.log("fetchCurrentField 异常", e)
+	}
+}
+
+// 获取地块列表
+const getFieldList = (isRefresh = false) => {
+	console.log("执行情况")
+	if (isRefresh) {
+		pageNum.value = 1
+		blocks.value = []
+		hasMore.value = true
+	}
+
+	if (!hasMore.value && !isRefresh) {
+		return
+	}
+
+	if (pageNum.value === 1) {
+		isLoading.value = true
+	} else {
+		loadingMore.value = true
+		isLoadingTip.value = true
+	}
+	console.log("当前页码值:", pageNum.value)
+	
+	fetchUserFieldList(
+		pageNum.value,
+		pageSize.value
+	).then((res) => {
+		if (pageNum.value === 1) {
+			isLoading.value = false
+		} else {
+			loadingMore.value = false
+		}
+		console.log("地块res", res)
+
+		if (res.data && res.data.code === 200 && res.data.data) {
+			totalCount.value = res.data.data.total || 0
 
-		computed: {
-			filteredBlocks() {
-				return this.blocks;
+			if (pageNum.value === 1) {
+				blocks.value = res.data.data.list
+			} else {
+				blocks.value = [...blocks.value, ...res.data.data.list]
 			}
-		},
-
-		mounted() {
-			// 获取当前选中地块
-			// this.fetchCurrentField();
-
-			// 获取地块列表
-			this.getFieldList(true);
-		},
-
-		// 添加页面触底事件处理函数
-		onReachBottom() {
-			this.loadMore();
-		},
-
-		// 下拉刷新
-		onPullDownRefresh() {
-			this.refreshing = true;
-			// 重置页码并刷新列表
-			this.getFieldList(true);
-
-			// 延迟关闭刷新动画
-			setTimeout(() => {
-				this.refreshing = false;
-				uni.stopPullDownRefresh();
-			}, 1000);
-		},
-
-		methods: {
-			// 获取当前选中的地块
-			fetchCurrentField() {
-				try {
-					// 获取当前登录用户的默认地块
-					getUserCurrentField().then((res) => {
-						console.log("fetchCurrentField", res);
-						if (res.data && res.data.code === 200 && res.data.data) {
-							this.currentBlock = res.data.data;
-						} else {
-							// 如果没有当前选中地块,则在获取列表成功后设置第一个为当前地块
-							console.log('未查询到当前地块,将使用列表第一个地块');
-						}
-					}).catch(err => {
-						console.error('获取当前地块失败', err);
-					});
-				} catch (e) {
-					console.log("fetchCurrentField 异常", e);
-				}
-			},
-
-			// 获取地块列表
-			getFieldList(isRefresh = false) {
-				console.log("执行情况");
-				// 刷新时重置页码
-				if (isRefresh) {
-					this.pageNum = 1;
-					this.blocks = [];
-					this.hasMore = true;
-				}
 
-				if (!this.hasMore && !isRefresh) {
-					return; // 如果没有更多数据且不是刷新,则不请求
-				}
+			hasMore.value = blocks.value.length < totalCount.value
 
-				// 标记正在加载
-				if (this.pageNum === 1) {
-					this.isLoading = true;
-				} else {
-					this.loadingMore = true;
-					this.isLoadingTip = true;
-				}
-				console.log("当前页码值:", this.pageNum);
-				// 获取当前登录用户关联的地块列表
-				fetchUserFieldList(
-					this.pageNum,
-					this.pageSize
-				).then((res) => {
-					if (this.pageNum === 1) {
-						this.isLoading = false;
-					} else {
-						this.loadingMore = false;
-					}
-					console.log("地块res", res);
-
-
-					if (res.data && res.data.code === 200 && res.data.data) {
-						// 获取总数
-						this.totalCount = res.data.data.total || 0;
-
-						// 追加数据,而不是替换
-						if (this.pageNum === 1) {
-							this.blocks = res.data.data.list;
-						} else {
-							this.blocks = [...this.blocks, ...res.data.data.list];
-						}
-
-						// 判断是否还有更多数据
-						this.hasMore = this.blocks.length < this.totalCount;
-
-						// 尝试从本地存储中读取上次选择的地块
-						if (this.pageNum === 1) {
-							this.loadSavedBlock();
-
-							// 如果当前没有选中地块,则设置第一个为当前地块
-							if (!this.currentBlock.id && this.blocks.length > 0) {
-								this.currentBlock = this.blocks[0];
-
-								// 保存当前选择的地块到本地存储
-								this.saveCurrentBlockToStorage(this.currentBlock);
-							}
-						}
-
-						// 页码加1,为下次加载做准备
-						this.pageNum++;
-					} else {
-						this.isLoading = false;
-						this.loadingMore = false;
-						this.hasMore = false;
-						console.error('获取地块列表失败', res);
-						uni.showToast({
-							title: '获取地块列表失败',
-							icon: 'none'
-						});
-					}
+			if (pageNum.value === 1) {
+				loadSavedBlock()
 
-					// 2秒后隐藏加载提示
-					setTimeout(() => {
-						this.isLoadingTip = false;
-					}, 2000);
-				}).catch(err => {
-					this.isLoading = false;
-					this.loadingMore = false;
-					this.hasMore = false;
-					console.error('获取地块列表失败', err);
-					uni.showToast({
-						title: '获取地块列表失败',
-						icon: 'none'
-					});
-
-					// 隐藏加载提示
-					this.isLoadingTip = false;
-				});
-			},
-
-			// 根据关键字搜索地块
-			searchFields(keyword) {
-				console.log("keyword",keyword);
-				if (!keyword) {
-					this.getFieldList(true);
-					return;
+				if (!currentBlock.id && blocks.value.length > 0) {
+					Object.assign(currentBlock, blocks.value[0])
+					saveCurrentBlockToStorage(currentBlock)
 				}
+			}
 
-				// 重置分页数据
-				this.pageNum = 1;
-				this.blocks = [];
-				this.hasMore = true;
-				this.isLoading = true;
-
-				// 使用用户关联地块搜索接口
-				searchUserField({
-					keyword: keyword,
-					pageNum: this.pageNum,
-					pageSize: this.pageSize
-				}).then((res) => {
-					this.isLoading = false;
-
-					if (res.data && res.data.code === 200 && res.data.data.list) {
-						this.blocks = res.data.data.list;
-						// 搜索结果可能没有总数信息,根据返回数据判断是否有更多
-						this.hasMore = res.data.data.total >= this.pageSize;
-
-						// 增加页码为下次加载做准备
-						this.pageNum++;
-					}
-				}).catch(err => {
-					this.isLoading = false;
-					this.hasMore = false;
-					console.error('搜索地块失败', err);
-				});
-			},
-
-			goBack() {
-				uni.navigateBack();
-			},
-
-			handleSearch() {
-				// 防抖处理,避免频繁请求
-				if (this.searchTimer) {
-					clearTimeout(this.searchTimer);
-				}
+			pageNum.value++
+		} else {
+			isLoading.value = false
+			loadingMore.value = false
+			hasMore.value = false
+			console.error('获取地块列表失败', res)
+			uni.showToast({
+				title: '获取地块列表失败',
+				icon: 'none'
+			})
+		}
 
-				this.searchTimer = setTimeout(() => {
-					this.searchFields(this.searchKeyword);
-				}, 500);
-			},
-
-			clearSearch() {
-				this.searchKeyword = '';
-				this.getFieldList(true);
-			},
-
-			handleBlockClick(block) {
-				// 如果点击当前已选中的地块,不做任何操作
-				if (this.currentBlock.id === block.id) return;
-
-				// 弹出确认框
-				uni.showModal({
-					title: '切换地块',
-					content: '是否切换到该地块?切换后将查看该地块的相关设备数据。',
-					cancelText: '取消',
-					confirmText: '确定',
-					success: (res) => {
-						if (res.confirm) {
-							this.selectBlock(block);
-						}
-					}
-				});
-			},
-
-			selectBlock(block) {
-				this.currentBlock = block;
-				console.log("选择地块信息:",block);
-				// 保存当前选择的地块到本地存储
-				this.saveCurrentBlockToStorage(block);
-
-				// 触发选择事件,传递地块ID
-				this.$emit('selectBlock', block.id);
-
-				// 获取页面参数,检查是否需要返回
-				const pages = getCurrentPages();
-				const currentPage = pages[pages.length - 1];
-				let eventChannel;
-
-				// 尝试获取页面参数和事件通道
-				let shouldReturn = true; // 默认行为是返回
-				try {
-					const options = currentPage.options || {};
-					// 获取事件通道(如果存在)
-					eventChannel = currentPage.getOpenerEventChannel && currentPage.getOpenerEventChannel();
-
-					// 如果有redirect参数,则跳转到指定页面而不是返回
-					if (options.redirect) {
-						shouldReturn = false;
-						uni.redirectTo({
-							url: decodeURIComponent(options.redirect),
-							success: () => {
-								// 跳转成功后传递选中的地块数据
-								if (eventChannel) {
-									eventChannel.emit('selectBlockResult', {
-										success: true,
-										blockId: block.id,
-										blockData: block
-									});
-								}
-							}
-						});
-					} else if (options.noReturn === 'true') {
-						// 如果设置了noReturn参数,则不执行返回操作
-						shouldReturn = false;
+		setTimeout(() => {
+			isLoadingTip.value = false
+		}, 2000)
+	}).catch(err => {
+		isLoading.value = false
+		loadingMore.value = false
+		hasMore.value = false
+		console.error('获取地块列表失败', err)
+		uni.showToast({
+			title: '获取地块列表失败',
+			icon: 'none'
+		})
+		isLoadingTip.value = false
+	})
+}
+
+// 根据关键字搜索地块
+const searchFields = (keyword) => {
+	console.log("keyword", keyword)
+	if (!keyword) {
+		getFieldList(true)
+		return
+	}
+
+	pageNum.value = 1
+	blocks.value = []
+	hasMore.value = true
+	isLoading.value = true
+
+	searchUserField({
+		keyword: keyword,
+		pageNum: pageNum.value,
+		pageSize: pageSize.value
+	}).then((res) => {
+		isLoading.value = false
+
+		if (res.data && res.data.code === 200 && res.data.data.list) {
+			blocks.value = res.data.data.list
+			hasMore.value = res.data.data.total >= pageSize.value
+			pageNum.value++
+		}
+	}).catch(err => {
+		isLoading.value = false
+		hasMore.value = false
+		console.error('搜索地块失败', err)
+	})
+}
+
+const goBack = () => {
+	uni.navigateBack()
+}
+
+const handleSearch = () => {
+	if (searchTimer.value) {
+		clearTimeout(searchTimer.value)
+	}
+
+	searchTimer.value = setTimeout(() => {
+		searchFields(searchKeyword.value)
+	}, 500)
+}
+
+const clearSearch = () => {
+	searchKeyword.value = ''
+	getFieldList(true)
+}
+
+const handleBlockClick = (block) => {
+	if (currentBlock.id === block.id) return
+
+	uni.showModal({
+		title: '切换地块',
+		content: '是否切换到该地块?切换后将查看该地块的相关设备数据。',
+		cancelText: '取消',
+		confirmText: '确定',
+		success: (res) => {
+			if (res.confirm) {
+				selectBlock(block)
+			}
+		}
+	})
+}
+
+const selectBlock = (block) => {
+	Object.assign(currentBlock, block)
+	console.log("选择地块信息:", block)
+	saveCurrentBlockToStorage(block)
+
+	const pages = getCurrentPages()
+	const currentPage = pages[pages.length - 1]
+	let eventChannel
+
+	let shouldReturn = true
+	try {
+		const options = currentPage.options || {}
+		eventChannel = currentPage.getOpenerEventChannel && currentPage.getOpenerEventChannel()
+
+		if (options.redirect) {
+			shouldReturn = false
+			uni.redirectTo({
+				url: decodeURIComponent(options.redirect),
+				success: () => {
+					if (eventChannel) {
+						eventChannel.emit('selectBlockResult', {
+							success: true,
+							blockId: block.id,
+							blockData: block
+						})
 					}
-				} catch (e) {
-					console.error('获取页面参数失败', e);
-				}
-
-				// 如果需要返回上一页,则执行返回操作
-				if (shouldReturn) {
-					setTimeout(() => {
-						uni.navigateBack({
-							success: () => {
-								// 返回成功后传递选中的地块数据
-								if (eventChannel) {
-									eventChannel.emit('selectBlockResult', {
-										success: true,
-										blockId: block.id,
-										blockData: block
-									});
-								}
-							}
-						});
-					}, 300);
 				}
-			},
-
-			// 保存当前选择的地块到本地存储
-			saveCurrentBlockToStorage(block) {
-				try {
-					this.currentBlock = block
-					console.log("this.currentBlock", this.currentBlock);
-					console.log("block", block);
-					storage.setPlots(JSON.stringify({
-						id: block.id,
-						code: block.code,
-						name: block.name,
-						growCrops: block.crop,
-						managerName:block.manager,
-						size: block.area,
-						farmId:block.farmId,
-						timestamp: Date.now()
-					}))
-				} catch (e) {
-					console.error('保存地块选择状态失败', e);
-				}
-			},
-
-			// 从本地存储加载上次选择的地块
-			loadSavedBlock() {
-				try {
-					const savedPlot = storage.getPlots();
-					if (savedPlot) {
-						const plotData = JSON.parse(savedPlot);
-						// 如果存储的地块在当前地块列表中,则设置为当前选中地块
-						const matchedBlock = this.blocks.find(block => block.id == plotData.id);
-						if (matchedBlock) {
-							this.currentBlock = matchedBlock;
-						}
+			})
+		} else if (options.noReturn === 'true') {
+			shouldReturn = false
+		}
+	} catch (e) {
+		console.error('获取页面参数失败', e)
+	}
+
+	if (shouldReturn) {
+		setTimeout(() => {
+			uni.navigateBack({
+				success: () => {
+					if (eventChannel) {
+						eventChannel.emit('selectBlockResult', {
+							success: true,
+							blockId: block.id,
+							blockData: block
+						})
 					}
-				} catch (e) {
-					console.error('读取保存的地块失败', e);
 				}
-			},
-
-			getStatusClass(status) {
-				const statusMap = {
-					'active': 'status-active',
-					'idle': 'status-idle',
-					'maintenance': 'status-maintenance'
-				}
-				return statusMap[status] || '';
-			},
-
-			getStatusText(status) {
-				const statusMap = {
-					'active': '使用中',
-					'idle': '闲置',
-					'maintenance': '维护中'
-				}
-				return statusMap[status] || '未知';
-			},
-
-			// 可以被父组件调用的方法
-			onBack(callback) {
-				if (typeof callback === 'function') {
-					callback(this.currentBlock.id);
-				}
-			},
-
-			// 加载更多数据
-			loadMore() {
-				if (this.loadingMore || !this.hasMore) return;
-				this.getFieldList();
+			})
+		}, 300)
+	}
+}
+
+const saveCurrentBlockToStorage = (block) => {
+	try {
+		Object.assign(currentBlock, block)
+		console.log("this.currentBlock", currentBlock)
+		console.log("block", block)
+		storage.setPlots(JSON.stringify({
+			id: block.id,
+			code: block.code,
+			name: block.name,
+			growCrops: block.crop,
+			managerName: block.manager,
+			size: block.area,
+			farmId: block.farmId,
+			timestamp: Date.now()
+		}))
+	} catch (e) {
+		console.error('保存地块选择状态失败', e)
+	}
+}
+
+const loadSavedBlock = () => {
+	try {
+		const savedPlot = storage.getPlots()
+		if (savedPlot) {
+			const plotData = JSON.parse(savedPlot)
+			const matchedBlock = blocks.value.find(block => block.id == plotData.id)
+			if (matchedBlock) {
+				Object.assign(currentBlock, matchedBlock)
 			}
 		}
-	}
+	} catch (e) {
+		console.error('读取保存的地块失败', e)
+	}
+}
+
+const getStatusClass = (status) => {
+	const statusMap = {
+		'active': 'status-active',
+		'idle': 'status-idle',
+		'maintenance': 'status-maintenance'
+	}
+	return statusMap[status] || ''
+}
+
+const getStatusText = (status) => {
+	const statusMap = {
+		'active': '使用中',
+		'idle': '闲置',
+		'maintenance': '维护中'
+	}
+	return statusMap[status] || '未知'
+}
+
+const onBack = (callback) => {
+	if (typeof callback === 'function') {
+		callback(currentBlock.id)
+	}
+}
+
+const loadMore = () => {
+	if (loadingMore.value || !hasMore.value) return
+	getFieldList()
+}
+
+// uni-app 生命周期
+onMounted(() => {
+	getFieldList(true)
+})
+
+// uni-app 页面生命周期
+const onReachBottom = () => {
+	loadMore()
+}
+
+const onPullDownRefresh = () => {
+	refreshing.value = true
+	getFieldList(true)
+
+	setTimeout(() => {
+		refreshing.value = false
+		uni.stopPullDownRefresh()
+	}, 1000)
+}
+
+// 导出页面生命周期方法供 uni-app 调用
+defineExpose({
+	onReachBottom,
+	onPullDownRefresh
+})
 </script>
 
 <style>

+ 2 - 4
pages/privacy/index.vue

@@ -84,10 +84,8 @@
   </view>
 </template>
 
-<script>
-export default {
-  // 隐私政策页面无需特殊逻辑,但仍需符合Vue 2格式
-};
+<script setup>
+// 隐私政策页面 - 纯静态内容,无需响应式数据或方法
 </script>
 
 <style lang="scss">

+ 10 - 17
pages/service/certification.vue

@@ -133,24 +133,17 @@
   </view>
 </template>
 
-<script>
-export default {
-  name: 'Certification',
-  methods: {
-    // 拨打电话
-    makePhoneCall() {
-      uni.makePhoneCall({
-        phoneNumber: '400-123-4567',
-        fail: (err) => {
-          console.log('拨打电话失败:', err);
-          uni.showToast({
-            title: '拨打电话失败',
-            icon: 'none'
-          });
-        }
-      });
+<script setup>
+const makePhoneCall = () => {
+  uni.makePhoneCall({
+    phoneNumber: '400-123-4567',
+    fail: () => {
+      uni.showToast({
+        title: '拨打电话失败',
+        icon: 'none'
+      })
     }
-  }
+  })
 }
 </script>
 

+ 228 - 231
pages/service/expert-chat.vue

@@ -136,251 +136,248 @@
   </view>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      expertId: '',
-      expertName: '',
-      inputMessage: '',
-      selectedImages: [],
-      scrollTop: 0,
-      isTyping: false,
-      
-      expertInfo: {
-        id: '',
-        name: '',
-        avatar: '/static/icons/professor.png'
-      },
-      
-      // 消息列表
-      messageList: [
-        {
-          id: 1,
-          type: 'expert',
-          contentType: 'text',
-          content: '您好!我是张教授,很高兴为您提供农业技术咨询服务。请问您遇到了什么问题?',
-          timestamp: Date.now() - 3600000
-        },
-        {
-          id: 2,
-          type: 'user',
-          contentType: 'text',
-          content: '教授您好,我想咨询一下水稻种植的相关问题',
-          timestamp: Date.now() - 3500000
-        },
-        {
-          id: 3,
-          type: 'expert',
-          contentType: 'text',
-          content: '好的,请您详细描述一下具体是什么问题?比如是关于播种、施肥、病虫害防治还是其他方面的?',
-          timestamp: Date.now() - 3400000
-        }
-      ]
-    }
+<script setup>
+import { ref, computed, nextTick, onMounted } from 'vue'
+
+const expertId = ref('')
+const expertName = ref('')
+const inputMessage = ref('')
+const selectedImages = ref([])
+const scrollTop = ref(0)
+const isTyping = ref(false)
+
+const expertInfo = ref({
+  id: '',
+  name: '',
+  avatar: '/static/icons/professor.png'
+})
+
+// 消息列表
+const messageList = ref([
+  {
+    id: 1,
+    type: 'expert',
+    contentType: 'text',
+    content: '您好!我是张教授,很高兴为您提供农业技术咨询服务。请问您遇到了什么问题?',
+    timestamp: Date.now() - 3600000
   },
-  
-  computed: {
-    canSend() {
-      return this.inputMessage.trim().length > 0 || this.selectedImages.length > 0;
-    }
+  {
+    id: 2,
+    type: 'user',
+    contentType: 'text',
+    content: '教授您好,我想咨询一下水稻种植的相关问题',
+    timestamp: Date.now() - 3500000
   },
+  {
+    id: 3,
+    type: 'expert',
+    contentType: 'text',
+    content: '好的,请您详细描述一下具体是什么问题?比如是关于播种、施肥、病虫害防治还是其他方面的?',
+    timestamp: Date.now() - 3400000
+  }
+])
+
+const canSend = computed(() => {
+  return inputMessage.value.trim().length > 0 || selectedImages.value.length > 0
+})
+
+// 页面加载
+onMounted(() => {
+  const pages = getCurrentPages()
+  const currentPage = pages[pages.length - 1]
+  const options = currentPage.options
+  
+  if (options.expertId) {
+    expertId.value = options.expertId
+    expertName.value = decodeURIComponent(options.expertName || '专家')
+    loadExpertInfo()
+  }
+  
+  // 设置页面标题
+  uni.setNavigationBarTitle({
+    title: expertName.value
+  })
   
-  onLoad(options) {
-    if (options.expertId) {
-      this.expertId = options.expertId;
-      this.expertName = decodeURIComponent(options.expertName || '专家');
-      this.loadExpertInfo();
+  // 滚动到底部
+  nextTick(() => {
+    scrollToBottom()
+  })
+})
+
+// 加载专家信息
+const loadExpertInfo = () => {
+  expertInfo.value = {
+    id: expertId.value,
+    name: expertName.value,
+    avatar: '/static/icons/professor.png'
+  }
+}
+
+// 发送消息
+const sendMessage = () => {
+  if (!canSend.value) return
+  
+  // 发送文本消息
+  if (inputMessage.value.trim()) {
+    const textMessage = {
+      id: Date.now(),
+      type: 'user',
+      contentType: 'text',
+      content: inputMessage.value.trim(),
+      timestamp: Date.now()
     }
-    
-    // 设置页面标题
-    uni.setNavigationBarTitle({
-      title: this.expertName
-    });
-    
-    // 滚动到底部
-    this.$nextTick(() => {
-      this.scrollToBottom();
-    });
-  },
+    messageList.value.push(textMessage)
+    inputMessage.value = ''
+  }
   
-  methods: {
-    // 加载专家信息
-    loadExpertInfo() {
-      this.expertInfo = {
-        id: this.expertId,
-        name: this.expertName,
-        avatar: '/static/icons/professor.png'
-      };
-    },
-    
-    // 发送消息
-    sendMessage() {
-      if (!this.canSend) return;
-      
-      // 发送文本消息
-      if (this.inputMessage.trim()) {
-        const textMessage = {
-          id: Date.now(),
-          type: 'user',
-          contentType: 'text',
-          content: this.inputMessage.trim(),
-          timestamp: Date.now()
-        };
-        this.messageList.push(textMessage);
-        this.inputMessage = '';
-      }
-      
-      // 发送图片消息
-      if (this.selectedImages.length > 0) {
-        this.selectedImages.forEach(image => {
-          const imageMessage = {
-            id: Date.now() + Math.random(),
-            type: 'user',
-            contentType: 'image',
-            content: image,
-            timestamp: Date.now()
-          };
-          this.messageList.push(imageMessage);
-        });
-        this.selectedImages = [];
+  // 发送图片消息
+  if (selectedImages.value.length > 0) {
+    selectedImages.value.forEach(image => {
+      const imageMessage = {
+        id: Date.now() + Math.random(),
+        type: 'user',
+        contentType: 'image',
+        content: image,
+        timestamp: Date.now()
       }
-      
-      // 滚动到底部
-      this.$nextTick(() => {
-        this.scrollToBottom();
-      });
-      
-      // 模拟专家回复
-      this.simulateExpertReply();
-    },
-    
-    // 模拟专家回复
-    simulateExpertReply() {
-      this.isTyping = true;
-      
-      setTimeout(() => {
-        this.isTyping = false;
-        
-        const replies = [
-          '根据您的描述,我建议您可以从以下几个方面来处理...',
-          '这个问题比较常见,通常是由于以下原因造成的...',
-          '从您提供的信息来看,建议您先检查一下土壤情况...',
-          '我理解您的担心,这种情况下最好的解决方案是...'
-        ];
-        
-        const randomReply = replies[Math.floor(Math.random() * replies.length)];
-        
-        const expertMessage = {
-          id: Date.now(),
-          type: 'expert',
-          contentType: 'text',
-          content: randomReply,
-          timestamp: Date.now()
-        };
-        
-        this.messageList.push(expertMessage);
-        
-        this.$nextTick(() => {
-          this.scrollToBottom();
-        });
-      }, 2000);
-    },
-    
-    // 选择图片
-    chooseImage() {
-      const maxImages = 3 - this.selectedImages.length;
-      if (maxImages <= 0) {
-        uni.showToast({
-          title: '最多可选择3张图片',
-          icon: 'none'
-        });
-        return;
-      }
-      
-      uni.chooseImage({
-        count: maxImages,
-        sizeType: ['compressed'],
-        sourceType: ['album', 'camera'],
-        success: (res) => {
-          this.selectedImages = this.selectedImages.concat(res.tempFilePaths);
-        }
-      });
-    },
-    
-    // 移除图片
-    removeImage(index) {
-      this.selectedImages.splice(index, 1);
-    },
-    
-    // 预览图片
-    previewImage(current) {
-      // 收集所有图片消息的URL
-      const imageUrls = this.messageList
-        .filter(msg => msg.contentType === 'image')
-        .map(msg => msg.content);
-      
-      uni.previewImage({
-        current: current,
-        urls: imageUrls
-      });
-    },
-    
-    // 滚动到底部
-    scrollToBottom() {
-      const query = uni.createSelectorQuery().in(this);
-      query.select('.message-wrapper').boundingClientRect();
-      query.exec((res) => {
-        if (res[0]) {
-          this.scrollTop = res[0].height;
-        }
-      });
-    },
+      messageList.value.push(imageMessage)
+    })
+    selectedImages.value = []
+  }
+  
+  // 滚动到底部
+  nextTick(() => {
+    scrollToBottom()
+  })
+  
+  // 模拟专家回复
+  simulateExpertReply()
+}
+
+// 模拟专家回复
+const simulateExpertReply = () => {
+  isTyping.value = true
+  
+  setTimeout(() => {
+    isTyping.value = false
     
-    // 滚动事件
-    onScroll(e) {
-      // 可以在这里处理滚动相关逻辑
-    },
+    const replies = [
+      '根据您的描述,我建议您可以从以下几个方面来处理...',
+      '这个问题比较常见,通常是由于以下原因造成的...',
+      '从您提供的信息来看,建议您先检查一下土壤情况...',
+      '我理解您的担心,这种情况下最好的解决方案是...'
+    ]
     
-    // 输入框聚焦
-    onInputFocus() {
-      setTimeout(() => {
-        this.scrollToBottom();
-      }, 300);
-    },
+    const randomReply = replies[Math.floor(Math.random() * replies.length)]
     
-    // 输入框失焦
-    onInputBlur() {
-      // 可以在这里处理失焦逻辑
-    },
+    const expertMessage = {
+      id: Date.now(),
+      type: 'expert',
+      contentType: 'text',
+      content: randomReply,
+      timestamp: Date.now()
+    }
     
-    // 格式化时间
-    formatTime(timestamp) {
-      const date = new Date(timestamp);
-      const now = new Date();
-      const diff = now - date;
-      
-      if (diff < 60000) { // 1分钟内
-        return '刚刚';
-      } else if (diff < 3600000) { // 1小时内
-        return `${Math.floor(diff / 60000)}分钟前`;
-      } else if (date.toDateString() === now.toDateString()) { // 今天
-        return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' });
-      } else {
-        return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
-      }
-    },
+    messageList.value.push(expertMessage)
     
-    // 获取当前日期
-    getCurrentDate() {
-      return new Date().toLocaleDateString('zh-CN', { 
-        year: 'numeric', 
-        month: 'long', 
-        day: 'numeric' 
-      });
+    nextTick(() => {
+      scrollToBottom()
+    })
+  }, 2000)
+}
+
+// 选择图片
+const chooseImage = () => {
+  const maxImages = 3 - selectedImages.value.length
+  if (maxImages <= 0) {
+    uni.showToast({
+      title: '最多可选择3张图片',
+      icon: 'none'
+    })
+    return
+  }
+  
+  uni.chooseImage({
+    count: maxImages,
+    sizeType: ['compressed'],
+    sourceType: ['album', 'camera'],
+    success: (res) => {
+      selectedImages.value = selectedImages.value.concat(res.tempFilePaths)
     }
+  })
+}
+
+// 移除图片
+const removeImage = (index) => {
+  selectedImages.value.splice(index, 1)
+}
+
+// 预览图片
+const previewImage = (current) => {
+  // 收集所有图片消息的URL
+  const imageUrls = messageList.value
+    .filter(msg => msg.contentType === 'image')
+    .map(msg => msg.content)
+  
+  uni.previewImage({
+    current: current,
+    urls: imageUrls
+  })
+}
+
+// 滚动到底部
+const scrollToBottom = () => {
+  const query = uni.createSelectorQuery()
+  query.select('.message-wrapper').boundingClientRect()
+  query.exec((res) => {
+    if (res[0]) {
+      scrollTop.value = res[0].height
+    }
+  })
+}
+
+// 滚动事件
+const onScroll = (e) => {
+  // 可以在这里处理滚动相关逻辑
+}
+
+// 输入框聚焦
+const onInputFocus = () => {
+  setTimeout(() => {
+    scrollToBottom()
+  }, 300)
+}
+
+// 输入框失焦
+const onInputBlur = () => {
+  // 可以在这里处理失焦逻辑
+}
+
+// 格式化时间
+const formatTime = (timestamp) => {
+  const date = new Date(timestamp)
+  const now = new Date()
+  const diff = now - date
+  
+  if (diff < 60000) { // 1分钟内
+    return '刚刚'
+  } else if (diff < 3600000) { // 1小时内
+    return `${Math.floor(diff / 60000)}分钟前`
+  } else if (date.toDateString() === now.toDateString()) { // 今天
+    return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' })
+  } else {
+    return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
   }
 }
+
+// 获取当前日期
+const getCurrentDate = () => {
+  return new Date().toLocaleDateString('zh-CN', { 
+    year: 'numeric', 
+    month: 'long', 
+    day: 'numeric' 
+  })
+}
 </script>
 
 <style lang="scss">

+ 71 - 72
pages/service/expert-detail.vue

@@ -85,81 +85,80 @@
   </view>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      expertId: '',
-      expertInfo: {
-        id: '',
-        name: '',
-        title: '',
-        unit: '',
-        avatar: '',
-        expertise: [],
-        introduction: '',
-        isOnline: false,
-        rating: 0,
-        consultCount: 0,
-        experience: 0,
-        serviceTime: {
-          weekday: '',
-          weekend: ''
-        }
-      }
-    }
-  },
+<script setup>
+import { ref, onMounted } from 'vue'
+
+const expertId = ref('')
+const expertInfo = ref({
+  id: '',
+  name: '',
+  title: '',
+  unit: '',
+  avatar: '',
+  expertise: [],
+  introduction: '',
+  isOnline: false,
+  rating: 0,
+  consultCount: 0,
+  experience: 0,
+  serviceTime: {
+    weekday: '',
+    weekend: ''
+  }
+})
+
+// 页面加载
+onMounted(() => {
+  const pages = getCurrentPages()
+  const currentPage = pages[pages.length - 1]
+  const options = currentPage.options
   
-  onLoad(options) {
-    if (options.id) {
-      this.expertId = options.id;
-      this.loadExpertDetail();
-    }
-  },
+  if (options.id) {
+    expertId.value = options.id
+    loadExpertDetail()
+  }
+})
+
+// 加载专家详情
+const loadExpertDetail = () => {
+  uni.showLoading({ title: '加载中...' })
   
-  methods: {
-    // 加载专家详情
-    loadExpertDetail() {
-      uni.showLoading({ title: '加载中...' });
-      
-      // 模拟API调用,实际应用中需要根据expertId从API获取数据
-      setTimeout(() => {
-                 const mockData = {
-           id: this.expertId,
-           name: '张教授',
-           title: '研究员',
-           unit: '农业科学院植物保护研究所',
-           avatar: '/static/icons/professor.png',
-           expertise: ['水稻种植', '病虫害防治', '土壤改良', '绿色防控', '有机农业'],
-          introduction: '农业技术推广研究员,从事农作物病虫害防治和绿色种植技术研究20余年。主持完成国家和省部级科研项目10余项,发表学术论文50多篇,获得农业技术推广奖3项。擅长水稻、小麦等主要粮食作物的病虫害综合防治,在生物防治、绿色防控等方面有丰富的实践经验。',
-          isOnline: true,
-          rating: 4.9,
-          consultCount: 156,
-          experience: 20,
-          serviceTime: {
-            weekday: '09:00-18:00',
-            weekend: '09:00-17:00'
-          }
-        };
-        
-        this.expertInfo = mockData;
-        
-        // 设置页面标题
-        uni.setNavigationBarTitle({
-          title: `${mockData.name} - 专家详情`
-        });
-        
-        uni.hideLoading();
-      }, 1000);
-    },
-    
-    // 开始咨询
-    startConsult() {
-      uni.navigateTo({
-        url: `/pages/service/expert-chat?expertId=${this.expertInfo.id}&expertName=${encodeURIComponent(this.expertInfo.name)}`
-      });
+  // 模拟API调用,实际应用中需要根据expertId从API获取数据
+  setTimeout(() => {
+    const mockData = {
+      id: expertId.value,
+      name: '张教授',
+      title: '研究员',
+      unit: '农业科学院植物保护研究所',
+      avatar: '/static/icons/professor.png',
+      expertise: ['水稻种植', '病虫害防治', '土壤改良', '绿色防控', '有机农业'],
+      introduction: '农业技术推广研究员,从事农作物病虫害防治和绿色种植技术研究20余年。主持完成国家和省部级科研项目10余项,发表学术论文50多篇,获得农业技术推广奖3项。擅长水稻、小麦等主要粮食作物的病虫害综合防治,在生物防治、绿色防控等方面有丰富的实践经验。',
+      isOnline: true,
+      rating: 4.9,
+      consultCount: 156,
+      experience: 20,
+      serviceTime: {
+        weekday: '09:00-18:00',
+        weekend: '09:00-17:00'
+      }
     }
-  }
+    
+    expertInfo.value = mockData
+    
+    // 设置页面标题
+    uni.setNavigationBarTitle({
+      title: `${mockData.name} - 专家详情`
+    })
+    
+    uni.hideLoading()
+  }, 1000)
+}
+
+// 开始咨询
+const startConsult = () => {
+  uni.navigateTo({
+    url: `/pages/service/expert-chat?expertId=${expertInfo.value.id}&expertName=${encodeURIComponent(expertInfo.value.name)}`
+  })
 }
 </script>
 

+ 121 - 129
pages/service/expert.vue

@@ -76,139 +76,131 @@
   </view>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      selectedCategory: 'all',
-      searchKeyword: '',
-      
-      // 分类选项
-      categoryList: [
-        { label: '全部', value: 'all' },
-        { label: '种植技术', value: 'planting' },
-        { label: '植物保护', value: 'protection' },
-        { label: '土壤肥料', value: 'soil' },
-        { label: '气象指导', value: 'weather' },
-        { label: '病虫害防治', value: 'pest' },
-        { label: '农机使用', value: 'machinery' }
-      ],
-      
-      // 专家列表数据
-      expertList: [
-        {
-          id: 1,
-          name: '张教授',
-          title: '研究员',
-          unit: '农业科学院',
-          avatar: '/static/icons/professor.png',
-          expertise: ['水稻种植', '病虫害防治', '土壤改良'],
-          category: 'planting',
-          isOnline: true,
-          rating: 4.9,
-          consultCount: 156
-        },
-        {
-          id: 2,
-          name: '李专家',
-          title: '高级农艺师',
-          unit: '植保站',
-          avatar: '/static/icons/professor.png',
-          expertise: ['植物保护', '农药使用', '生物防治'],
-          category: 'protection',
-          isOnline: false,
-          rating: 4.8,
-          consultCount: 203
-        },
-        {
-          id: 3,
-          name: '王老师',
-          title: '副教授',
-          unit: '农业大学',
-          avatar: '/static/icons/professor.png',
-          expertise: ['土壤检测', '施肥技术', '营养诊断'],
-          category: 'soil',
-          isOnline: true,
-          rating: 4.7,
-          consultCount: 89
-        },
-        {
-          id: 4,
-          name: '陈工程师',
-          title: '农机专家',
-          unit: '农机推广站',
-          avatar: '/static/icons/professor.png',
-          expertise: ['农机维修', '设备选型', '机械化作业'],
-          category: 'machinery',
-          isOnline: true,
-          rating: 4.6,
-          consultCount: 134
-        },
-        {
-          id: 5,
-          name: '刘博士',
-          title: '气象专家',
-          unit: '气象局',
-          avatar: '/static/icons/professor.png',
-          expertise: ['天气预报', '灾害预警', '农业气象'],
-          category: 'weather',
-          isOnline: false,
-          rating: 4.8,
-          consultCount: 178
-        }
-      ]
-    }
+<script setup>
+import { ref, computed } from 'vue'
+
+const selectedCategory = ref('all')
+const searchKeyword = ref('')
+
+// 分类选项
+const categoryList = [
+  { label: '全部', value: 'all' },
+  { label: '种植技术', value: 'planting' },
+  { label: '植物保护', value: 'protection' },
+  { label: '土壤肥料', value: 'soil' },
+  { label: '气象指导', value: 'weather' },
+  { label: '病虫害防治', value: 'pest' },
+  { label: '农机使用', value: 'machinery' }
+]
+
+// 专家列表数据
+const expertList = ref([
+  {
+    id: 1,
+    name: '张教授',
+    title: '研究员',
+    unit: '农业科学院',
+    avatar: '/static/icons/professor.png',
+    expertise: ['水稻种植', '病虫害防治', '土壤改良'],
+    category: 'planting',
+    isOnline: true,
+    rating: 4.9,
+    consultCount: 156
   },
-  
-  computed: {
-    filteredExpertList() {
-      let list = this.expertList;
-      
-      // 按分类筛选
-      if (this.selectedCategory !== 'all') {
-        list = list.filter(expert => expert.category === this.selectedCategory);
-      }
-      
-      // 按搜索关键词筛选
-      if (this.searchKeyword.trim()) {
-        const keyword = this.searchKeyword.trim().toLowerCase();
-        list = list.filter(expert => 
-          expert.name.toLowerCase().includes(keyword) ||
-          expert.expertise.some(tag => tag.toLowerCase().includes(keyword)) ||
-          expert.title.toLowerCase().includes(keyword) ||
-          expert.unit.toLowerCase().includes(keyword)
-        );
-      }
-      
-      return list;
-    }
+  {
+    id: 2,
+    name: '李专家',
+    title: '高级农艺师',
+    unit: '植保站',
+    avatar: '/static/icons/professor.png',
+    expertise: ['植物保护', '农药使用', '生物防治'],
+    category: 'protection',
+    isOnline: false,
+    rating: 4.8,
+    consultCount: 203
+  },
+  {
+    id: 3,
+    name: '王老师',
+    title: '副教授',
+    unit: '农业大学',
+    avatar: '/static/icons/professor.png',
+    expertise: ['土壤检测', '施肥技术', '营养诊断'],
+    category: 'soil',
+    isOnline: true,
+    rating: 4.7,
+    consultCount: 89
   },
+  {
+    id: 4,
+    name: '陈工程师',
+    title: '农机专家',
+    unit: '农机推广站',
+    avatar: '/static/icons/professor.png',
+    expertise: ['农机维修', '设备选型', '机械化作业'],
+    category: 'machinery',
+    isOnline: true,
+    rating: 4.6,
+    consultCount: 134
+  },
+  {
+    id: 5,
+    name: '刘博士',
+    title: '气象专家',
+    unit: '气象局',
+    avatar: '/static/icons/professor.png',
+    expertise: ['天气预报', '灾害预警', '农业气象'],
+    category: 'weather',
+    isOnline: false,
+    rating: 4.8,
+    consultCount: 178
+  }
+])
+
+const filteredExpertList = computed(() => {
+  let list = expertList.value
+  
+  // 按分类筛选
+  if (selectedCategory.value !== 'all') {
+    list = list.filter(expert => expert.category === selectedCategory.value)
+  }
   
-  methods: {
-    // 选择分类
-    selectCategory(category) {
-      this.selectedCategory = category;
-    },
-    
-    // 搜索处理
-    handleSearch() {
-      // 搜索逻辑已在computed中处理
-    },
-    
-    // 导航到专家详情页
-    navigateToDetail(expert) {
-      uni.navigateTo({
-        url: `/pages/service/expert-detail?id=${expert.id}&name=${encodeURIComponent(expert.name)}`
-      });
-    },
-    
-    // 开始咨询
-    startConsult(expert) {
-      uni.navigateTo({
-        url: `/pages/service/expert-chat?expertId=${expert.id}&expertName=${encodeURIComponent(expert.name)}`
-      });
-    }
+  // 按搜索关键词筛选
+  if (searchKeyword.value.trim()) {
+    const keyword = searchKeyword.value.trim().toLowerCase()
+    list = list.filter(expert => 
+      expert.name.toLowerCase().includes(keyword) ||
+      expert.expertise.some(tag => tag.toLowerCase().includes(keyword)) ||
+      expert.title.toLowerCase().includes(keyword) ||
+      expert.unit.toLowerCase().includes(keyword)
+    )
   }
+  
+  return list
+})
+
+// 选择分类
+const selectCategory = (category) => {
+  selectedCategory.value = category
+}
+
+// 搜索处理
+const handleSearch = () => {
+  // 搜索逻辑已在computed中处理
+}
+
+// 导航到专家详情页
+const navigateToDetail = (expert) => {
+  uni.navigateTo({
+    url: `/pages/service/expert-detail?id=${expert.id}&name=${encodeURIComponent(expert.name)}`
+  })
+}
+
+// 开始咨询
+const startConsult = (expert) => {
+  uni.navigateTo({
+    url: `/pages/service/expert-chat?expertId=${expert.id}&expertName=${encodeURIComponent(expert.name)}`
+  })
 }
 </script>
 

+ 10 - 16
pages/service/insurance.vue

@@ -166,23 +166,17 @@
   </view>
 </template>
 
-<script>
-export default {
-  methods: {
-    // 拨打电话
-    makePhoneCall() {
-      uni.makePhoneCall({
-        phoneNumber: '400-456-7890',
-        fail: (err) => {
-          console.log('拨打电话失败:', err);
-          uni.showToast({
-            title: '拨打电话失败',
-            icon: 'none'
-          });
-        }
-      });
+<script setup>
+const makePhoneCall = () => {
+  uni.makePhoneCall({
+    phoneNumber: '400-456-7890',
+    fail: (err) => {
+      uni.showToast({
+        title: '拨打电话失败',
+        icon: 'none'
+      })
     }
-  }
+  })
 }
 </script>
 

+ 90 - 184
pages/service/mall-detail.vue

@@ -76,196 +76,102 @@
   </view>
 </template>
 
-<script>
-	import { getMallById } from '@/api/services/mall.js';
-	import api from "@/config/api.js";
-export default {
-  data() {
-    return {
-      goodsId: '',
-      goodsDetail: {
-        // title: '高产玉米种子',
-        // description: '优质杂交种,抗病性强,适应性广',
-        // price: '68',
-        // originalPrice: '88',
-        // unit: '袋',
-        // specifications: '500g/袋,发芽率≥95%',
-        // features: [
-        //   '高产稳产,比普通品种增产15-20%',
-        //   '抗病性强,抗大小斑病、丝黑穗病',
-        //   '适应性广,适合多种土壤和气候条件',
-        //   '株型紧凑,抗倒伏能力强',
-        //   '籽粒饱满,商品性好'
-        // ],
-        // usage: '播种前先进行种子处理,选择肥沃、排水良好的土壤,按照每亩3-4千粒的密度播种。播种深度2-3厘米,播种后及时覆土镇压。生长期注意水肥管理,适时中耕除草。',
-        // detailImages: [
-        //   '/static/images/products/corn-seeds-new.jpg',
-        //   '/static/images/products/seeds-packets.jpg',
-        //   '/static/images/products/agriculture-tools.jpg'
-        // ]
-      },
-      goodsImages: []
-    }
-  },
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getMallById } from '@/api/services/mall.js'
+
+const goodsId = ref('')
+const goodsDetail = ref({})
+const goodsImages = ref([])
+
+// 页面加载
+onMounted(() => {
+  const pages = getCurrentPages()
+  const currentPage = pages[pages.length - 1]
+  const options = currentPage.options
   
-  onLoad(options) {
-    if (options.id) {
-      this.goodsId = options.id;
-      this.loadGoodsDetail(this.goodsId);
-    }
-    if (options.title) {
-      uni.setNavigationBarTitle({
-        title: decodeURIComponent(options.title)
-      });
-    }
-  },
+  if (options.id) {
+    goodsId.value = options.id
+    loadGoodsDetail(goodsId.value)
+  }
+  if (options.title) {
+    uni.setNavigationBarTitle({
+      title: decodeURIComponent(options.title)
+    })
+  }
+})
+
+// 加载商品详情
+const loadGoodsDetail = (id) => {
+  uni.showLoading({
+    title: '加载中'
+  })
   
-  methods: {
-	// getImageUrl(item) {
-	//   	// 默认返回url或path
-	// 	console.log("imageUrls",item);
-	// 	if(item == null) return
-	// 	// const imageUrls = item.split(',');
-		
-	//     return  imageUrls;
-	// },
-    // 加载商品详情
-    loadGoodsDetail(goodsId) {
-		uni.showLoading({
-			title: '加载中'
-		});
-		getMallById(goodsId).then(res=>{
-			if (res.data.code === 200) {
-			  const { data } = res.data;
-			  this.goodsDetail = data
-			  console.log("this.goodsDetail", this.goodsDetail);
-			  // 处理图片数据
-			  if (this.goodsDetail.detailImages && this.goodsDetail.swiperImages) {
-			    try {
-				  this.goodsImages = data.swiperImages
-			      // 尝试解析图片数据字符串
-			      const imageUrls = this.goodsDetail.detailImages.split(',');
-			      const swiperImages = this.goodsImages.split(',');
-			      this.goodsDetail.detailImages = imageUrls.filter(url => url && url.trim()).map(url => ({
-			        url: url.trim(), // 保存原始URL,显示时会通过getImageUrl方法处理
-			        status: 'success'
-			      }));
-				  this.goodsImages = swiperImages.filter(url => url && url.trim()).map(url => ({
-			        url: url.trim(), // 保存原始URL,显示时会通过getImageUrl方法处理
-			        status: 'success'
-			      }));
-			      console.log('解析后的图片数据:', this.goodsDetail.detailImages);
-			    } catch (e) {
-			      console.error('解析图片数据失败:', e);
-			      this.goodsDetail.detailImages = [];
-			      this.goodsDetail.swiperImages = [];
-			    }
-			  } else {
-			    this.goodsDetail.detailImages = [];
-			    this.goodsDetail.swiperImages = [];
-			  }
-			  uni.hideLoading();
-			} else {
-			  uni.showToast({
-			    title: res.data.msg || '获取商品信息失败',
-			    icon: 'none'
-			  });
-			}
-		})
-      // 这里可以根据goodsId从后端获取商品详情
-      // 目前使用模拟数据
-      // const goodsData = {
-      //   1: {
-      //     title: '高产玉米种子',
-      //     description: '优质杂交种,抗病性强,适应性广',
-      //     price: '68',
-      //     originalPrice: '88',
-      //     unit: '袋',
-      //     specifications: '500g/袋,发芽率≥95%',
-      //     features: [
-      //       '高产稳产,比普通品种增产15-20%',
-      //       '抗病性强,抗大小斑病、丝黑穗病',
-      //       '适应性广,适合多种土壤和气候条件',
-      //       '株型紧凑,抗倒伏能力强',
-      //       '籽粒饱满,商品性好'
-      //     ],
-      //     usage: '播种前先进行种子处理,选择肥沃、排水良好的土壤,按照每亩3-4千粒的密度播种。播种深度2-3厘米,播种后及时覆土镇压。生长期注意水肥管理,适时中耕除草。',
-      //     detailImages: [
-      //       '/static/images/products/corn-seeds-new.jpg',
-      //       '/static/images/products/seeds-packets.jpg',
-      //       '/static/images/products/agriculture-tools.jpg'
-      //     ]
-      //   }
-      // };
+  getMallById(id).then(res => {
+    if (res.data.code === 200) {
+      const { data } = res.data
+      goodsDetail.value = data
       
-      // if (goodsData[this.goodsId]) {
-      //   this.goodsDetail = goodsData[this.goodsId];
-      //   // 根据商品类型设置轮播图
-      //   this.setGoodsImages(this.goodsId);
-      // }
-    },
-    
-    // 设置商品轮播图
-    setGoodsImages(goodsId) {
-      const imageMap = {
-        '1': [ // 玉米种子
-          '/static/images/products/corn-seeds-new.jpg',
-          '/static/images/products/seeds-packets.jpg',
-          '/static/images/products/agriculture-tools.jpg',
-          '/static/images/products/wheat-seeds.jpg'
-        ],
-        '2': [ // 复合肥料
-          '/static/images/products/fertilizer.jpg',
-          '/static/images/products/organic-fertilizer-new.jpg',
-          '/static/images/products/soil-test.jpg',
-          '/static/images/products/greenhouse-film.jpg'
-        ],
-        '3': [ // 植物保护剂
-          '/static/images/products/plant-protection.jpg',
-          '/static/images/products/agriculture-tools.jpg',
-          '/static/images/products/pesticide.jpg',
-          '/static/images/products/insecticide.jpg'
-        ],
-        '4': [ // 水稻种子
-          '/static/images/products/seeds-packets.jpg',
-          '/static/images/products/rice-seeds.jpg',
-          '/static/images/products/corn-seeds-new.jpg',
-          '/static/images/products/wheat-seeds.jpg'
-        ]
-      };
+      // 处理图片数据
+      if (goodsDetail.value.detailImages && goodsDetail.value.swiperImages) {
+        try {
+          // 尝试解析图片数据字符串
+          const imageUrls = goodsDetail.value.detailImages.split(',')
+          const swiperImages = goodsDetail.value.swiperImages.split(',')
+          
+          goodsDetail.value.detailImages = imageUrls.filter(url => url && url.trim()).map(url => ({
+            url: url.trim(),
+            status: 'success'
+          }))
+          
+          goodsImages.value = swiperImages.filter(url => url && url.trim()).map(url => ({
+            url: url.trim(),
+            status: 'success'
+          }))
+        } catch (e) {
+          console.error('解析图片数据失败:', e)
+          goodsDetail.value.detailImages = []
+          goodsImages.value = []
+        }
+      } else {
+        goodsDetail.value.detailImages = []
+        goodsImages.value = []
+      }
       
-      if (imageMap[goodsId]) {
-        this.goodsImages = imageMap[goodsId];
+      uni.hideLoading()
+    } else {
+      uni.showToast({
+        title: res.data.msg || '获取商品信息失败',
+        icon: 'none'
+      })
+    }
+  })
+}
+
+// 预览图片
+const previewImage = (image, index) => {
+  const urls = goodsDetail.value.detailImages.map(item => item.url)
+  uni.previewImage({
+    urls: urls,
+    current: index
+  })
+}
+
+// 立即咨询
+const handleConsult = () => {
+  uni.showModal({
+    title: '咨询服务',
+    content: '您可以通过以下方式联系我们:\n1. 拨打客服热线:400-888-8888\n2. 在线客服(工作时间:9:00-18:00)',
+    confirmText: '拨打电话',
+    cancelText: '取消',
+    success: (res) => {
+      if (res.confirm) {
+        uni.makePhoneCall({
+          phoneNumber: '400-888-8888'
+        })
       }
-    },
-    
-    // 预览图片
-    previewImage(image, index) {
-		const urls = this.formData.images.map(item => item.url);
-		uni.previewImage({
-		  urls: urls,
-		  current: index
-		});
-		  
-    },
-    
-    // 立即咨询
-    handleConsult() {
-      uni.showModal({
-        title: '咨询服务',
-        content: '您可以通过以下方式联系我们:\n1. 拨打客服热线:400-888-8888\n2. 在线客服(工作时间:9:00-18:00)',
-        confirmText: '拨打电话',
-        cancelText: '取消',
-        success: (res) => {
-          if (res.confirm) {
-            uni.makePhoneCall({
-              phoneNumber: '400-888-8888'
-            });
-          }
-        }
-      });
     }
-  }
+  })
 }
 </script>
 

+ 131 - 217
pages/service/mall.vue

@@ -42,39 +42,6 @@
     </view>
 
     <!-- 商品展示区 -->
-	
-<!--    <view class="goods-container">
-		<scroll-view
-			scroll-y 
-			v-if="goodsList.length > 0"
-		    style="height: 100vh;" 
-		    @scrolltolower="loadMore">
-			<view class="goods-grid">
-			  <view 
-			    class="goods-card"
-			    v-for="item in filteredGoods" 
-			    :key="item.id"
-			    @click="navigateToDetail(item)"
-			  >
-			    <view class="goods-image">
-			      <image :src="getImageUrl(item.swiperImages)" mode="aspectFill"></image>
-			    </view>
-			    <view class="goods-info">
-			      <text class="goods-title">{{ item.title }}</text>
-			      <text class="goods-desc">{{ item.description }}</text>
-			      <view class="goods-price">
-			        <text class="price">¥{{ item.price }}</text>
-			        <!-- <text class="unit">/{{ item.unit }}</text> 
-			      </view>
-			      <view class="action-btn">了解更多</view>
-			    </view>
-			  </view>
-			</view>
-		</scroll-view>
-        <view class="no-more-data" v-if="noMoreData && goodsList.length > 0">
-          <text>没有更多数据了</text>
-        </view>
-    </view> -->
 	<view class="container">
 	  <!-- 有数据 -->
 	  <scroll-view
@@ -104,198 +71,143 @@
 	        </view>
 	      </view>
 	    </view>
-	
-	    
 	  </scroll-view>
-	<!-- 没有更多数据提示 -->
-	<view class="no-more-data" v-if="noMoreData">
-	  <text>没有更多数据了</text>
-	</view>
-	  <!-- 无数据 -->
-	 <!-- <view v-else class="empty">
-	    暂无数据
-	  </view> -->
+	  
+	  <!-- 没有更多数据提示 -->
+	  <view class="no-more-data" v-if="noMoreData">
+	    <text>没有更多数据了</text>
+	  </view>
 	</view>
-
   </view>
 </template>
 
-<script>
-	import { getMallList } from '@/api/services/mall.js';
-	import dictMixin from '@/utils/mixins/dictMixin';
-	import api from "@/config/api.js";
-export default {
-  mixins: [dictMixin],
-  data() {
-    return {
-	  dictTypeList: ['mall_product_category'],
-      searchKeyword: '',
-      activeCategory: -1,
-      scrollLeft: 0,
-      
-      // 商品列表
-      goodsList: [],
-	  pageNum: 1,
-	  pageSize: 10,
-	  noMoreData: false,
-	  isLoading: false,
-	  isRefreshing: false,
-    }
-  },
+<script setup>
+import { ref, computed, nextTick } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { getMallList } from '@/api/services/mall.js'
+import { useDict } from '@/utils/composables/useDict'
+
+const { dictData } = useDict(['mall_product_category'])
+
+const searchKeyword = ref('')
+const activeCategory = ref(-1)
+const scrollLeft = ref(0)
+const goodsList = ref([])
+const pageNum = ref(1)
+const pageSize = ref(10)
+const noMoreData = ref(false)
+const isLoading = ref(false)
+const isRefreshing = ref(false)
+
+const filteredGoods = computed(() => {
+  let goods = goodsList.value
   
-  computed: {
-    filteredGoods() {
-      let goods = this.goodsList;
-      
-      // 按分类筛选
-      if (this.activeCategory != -1) {
-        goods = goods.filter(item => item.productCategory == this.activeCategory);
-      }else{
-		  goods = goods.filter(item => item.isRecommended == 1);
-	  }
-      
-      // // 按搜索关键词筛选
-      // if (this.searchKeyword.trim()) {
-      //   const keyword = this.searchKeyword.trim().toLowerCase();
-      //   goods = goods.filter(item => 
-      //     item.title.toLowerCase().includes(keyword) || 
-      //     item.description.toLowerCase().includes(keyword)
-      //   );
-      // }
-      return goods;
-    }
-  },
+  if (activeCategory.value != -1) {
+    goods = goods.filter(item => item.productCategory == activeCategory.value)
+  } else {
+    goods = goods.filter(item => item.isRecommended == 1)
+  }
   
-  methods: {
-	getImageUrl(item) {
-	  	// 默认返回url或path
-		if(item == null) return
-		const imageUrls = item.split(',');
-		console.log("imageUrls",imageUrls);
-	    return  imageUrls[0];
-	},
-	loadMallData(keyword){
-		this.isLoading = true;
-		uni.showLoading({
-			title: '加载中'
-		});
-		// 构建查询参数
-		const params = {
-		  pageNum: this.pageNum,
-		  pageSize: this.pageSize,
-		  status: 1 // 已上架
-		  // productCategory: this.activeCategory,
-		  // recommend:1 // 查询推荐商品
-		};
-		if(keyword != null && keyword != ''){
-			params.searchKeyword = keyword
-		}
-		
-		// 分类逻辑
-		  if (this.activeCategory == -1) {
-			  // 默认场景:推荐查询
-			  params.isRecommended = 1;
-		    
-		  } else {
-		    // 分类查询:只带 productCategory
-		    params.productCategory = this.activeCategory;
-		  }
-		
-		getMallList(params).then(res => {
-		  if (res.data.code === 200) {
-		    const { rows, total } = res.data;
-		    if (this.pageNum === 1) {
-		      this.goodsList = rows;
-		    } else {
-		      this.goodsList = [...this.goodsList, ...rows];
-		    }
-		    console.log("this.goodsList.length >= total",this.goodsList.length);
-		    console.log("this.goodsList.length >= total",this.goodsList.length >= total);
-		    // 判断是否还有更多数据
-		    this.noMoreData = this.goodsList.length >= total;
-		    uni.hideLoading();
-		    // 统计待完成商品数量
-		    // this.countPendingTasks();
-		  } else {
-		    uni.showToast({
-		      title: res.data.msg || '获取商品列表失败',
-		      icon: 'none'
-		    });
-		  }
-		}).catch(err => {
-		  console.error('获取商品列表失败:', err);
-		  uni.showToast({
-		    title: '获取商品列表失败',
-		    icon: 'none'
-		  });
-		}).finally(() => {
-		  this.isLoading = false;
-		  this.isRefreshing = false;
-		});
-	},
-    // 上拉加载更多
-    loadMore() {
-      if (!this.isLoading && !this.noMoreData) {
-        this.pageNum++;
-        this.loadMallData();
-      }
-    },
-	// 切换分类
-    switchCategory(categoryId) {
-		console.log("dfdssdf",categoryId);
-      this.activeCategory = categoryId;
-	  this.goodsList = [] // 切换时置空数据数组
-      // 滚动到当前分类位置
-	  this.loadMallData(this.searchKeyword.trim())
-      this.$nextTick(() => {
-        this.scrollToActiveCategory(categoryId);
-      });
-    },
-    
-    // 滚动到激活的分类  
-    scrollToActiveCategory(categoryId) {
-      const index = this.dictData.mall_product_category.findIndex(item => item.id == categoryId);
-	  console.log("index",index);
-      if (index > -1) {
-        // 计算滚动位置,让当前分类居中显示
-        const itemWidth = 120; // 每个分类项大约120rpx宽度
-        const containerWidth = 750; // 屏幕宽度
-        const scrollLeft = Math.max(0, index * itemWidth - containerWidth / 2 + itemWidth / 2);
-        
-        // 设置scroll-view的scrollLeft
-        this.scrollLeft = scrollLeft;
+  return goods
+})
+
+const getImageUrl = (item) => {
+  if(item == null) return
+  const imageUrls = item.split(',')
+  return imageUrls[0]
+}
+
+const loadMallData = (keyword) => {
+  isLoading.value = true
+  uni.showLoading({
+    title: '加载中'
+  })
+  
+  const params = {
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+    status: 1
+  }
+  
+  if(keyword != null && keyword != ''){
+    params.searchKeyword = keyword
+  }
+  
+  if (activeCategory.value == -1) {
+    params.isRecommended = 1
+  } else {
+    params.productCategory = activeCategory.value
+  }
+  
+  getMallList(params).then(res => {
+    if (res.data.code === 200) {
+      const { rows, total } = res.data
+      if (pageNum.value === 1) {
+        goodsList.value = rows
+      } else {
+        goodsList.value = [...goodsList.value, ...rows]
       }
-    },
-    
-    // 搜索处理
-    handleSearch() {
-      // if (!this.searchKeyword.trim()) {
-      //   uni.showToast({
-      //     title: '请输入搜索关键词',
-      //     icon: 'none'
-      //   });
-      //   return;
-      // }
-	  const keyword = this.searchKeyword.trim().toLowerCase();
-	  this.loadMallData(keyword)
-    },
-    
-    // 导航到商品详情
-    navigateToDetail(goods) {
-      uni.navigateTo({
-        url: `/pages/service/mall-detail?id=${goods.id}&title=${encodeURIComponent(goods.title)}`
-      });
+      
+      noMoreData.value = goodsList.value.length >= total
+      uni.hideLoading()
+    } else {
+      uni.showToast({
+        title: res.data.msg || '获取商品列表失败',
+        icon: 'none'
+      })
     }
-  },
-  // 页面加载时获取数据
-  onLoad() {
-    this.loadMallData();
-  },
-  
-  onNavigationBarTitleText() {
-    return '农资商城';
+  }).catch(err => {
+    console.error('获取商品列表失败:', err)
+    uni.showToast({
+      title: '获取商品列表失败',
+      icon: 'none'
+    })
+  }).finally(() => {
+    isLoading.value = false
+    isRefreshing.value = false
+  })
+}
+
+const loadMore = () => {
+  if (!isLoading.value && !noMoreData.value) {
+    pageNum.value++
+    loadMallData()
   }
 }
+
+const switchCategory = (categoryId) => {
+  activeCategory.value = categoryId
+  goodsList.value = []
+  loadMallData(searchKeyword.value.trim())
+  nextTick(() => {
+    scrollToActiveCategory(categoryId)
+  })
+}
+
+const scrollToActiveCategory = (categoryId) => {
+  const index = dictData.value.mall_product_category.findIndex(item => item.id == categoryId)
+  if (index > -1) {
+    const itemWidth = 120
+    const containerWidth = 750
+    const scrollLeftValue = Math.max(0, index * itemWidth - containerWidth / 2 + itemWidth / 2)
+    scrollLeft.value = scrollLeftValue
+  }
+}
+
+const handleSearch = () => {
+  const keyword = searchKeyword.value.trim().toLowerCase()
+  loadMallData(keyword)
+}
+
+const navigateToDetail = (goods) => {
+  uni.navigateTo({
+    url: `/pages/service/mall-detail?id=${goods.id}&title=${encodeURIComponent(goods.title)}`
+  })
+}
+
+onLoad(() => {
+  loadMallData()
+})
 </script>
 
 <style lang="scss">
@@ -347,8 +259,8 @@ export default {
   height: 88rpx;
   width: 100%;
   overflow: hidden;
-  -webkit-overflow-scrolling: touch; /* iOS滑动优化 */
-  scroll-behavior: smooth; /* 平滑滚动 */
+  -webkit-overflow-scrolling: touch;
+  scroll-behavior: smooth;
 }
 
 .nav-list {
@@ -398,6 +310,7 @@ export default {
 .goods-container {
   padding: 20rpx;
 }
+
 .container {
   height: 100vh;
   display: flex;
@@ -483,6 +396,7 @@ export default {
   font-size: 28rpx;
   font-weight: 500;
 }
+
 .no-more-data{
 	text-align: center;
 	padding: 30rpx 0;
@@ -493,4 +407,4 @@ export default {
   color: #999;
   margin-left: 10rpx;
 }
-</style> 
+</style>

+ 174 - 196
pages/service/my-publish.vue

@@ -150,206 +150,184 @@
   </view>
 </template>
 
-<script>
-	import {
-		getProductInfoList
-	} from '@/api/services/productInfo.js';
-	import dictMixin from '@/utils/mixins/dictMixin';
-	import storage from "@/utils/storage.js";
-export default {
-	mixins: [dictMixin],
-  data() {
-    return {
-      activeTab: 'published',
-      searchKeyword: '',
-      purchaseSearchKeyword: '',
-      showPublishModal: false,
-      pageNum: 1,
-      pageSize: 10,
-      noMoreData: false,
-      isLoading: false,
-      isRefreshing: false,
-	  dictTypeList: ['agricultural_unit'],
-	  currentUserInfo: storage.getUserInfo(),
-      // 我的发布列表
-      publishedProductsList: [],
-      // 收购信息列表
-      purchaseList: [],
-	  productsList: [], // 二和一存储
-    }
-  },
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { getProductInfoList } from '@/api/services/productInfo.js'
+import { useDict } from '@/utils/composables/useDict'
+import storage from "@/utils/storage.js"
+
+// 使用字典
+const { dictData } = useDict(['agricultural_unit'])
+
+const activeTab = ref('published')
+const searchKeyword = ref('')
+const purchaseSearchKeyword = ref('')
+const showPublishModal = ref(false)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const noMoreData = ref(false)
+const isLoading = ref(false)
+const isRefreshing = ref(false)
+const currentUserInfo = storage.getUserInfo()
+const publishedProductsList = ref([])
+const purchaseList = ref([])
+const productsList = ref([])
+
+const filteredPublishedProducts = computed(() => {
+  let products = productsList.value
+  return products
+})
+
+const filteredPurchaseList = computed(() => {
+  let list = productsList.value
+  return list
+})
+
+// 页面显示时刷新数据
+const onShow = () => {
+  loadMyPublishInfo()
+}
+
+// 页面加载时获取数据
+onMounted(() => {
+  loadMyPublishInfo()
+})
+
+// 上拉加载更多
+const loadMore = () => {
+  if (!isLoading.value && !noMoreData.value) {
+    pageNum.value++
+    loadMyPublishInfo()
+  }
+}
+
+const getImageUrl = (item) => {
+  if (item == null) return
+  const imageUrls = item.split(',')
+  return imageUrls[0]
+}
+
+const getUnitLabel = (value) => {
+  const unit = dictData.agricultural_unit?.find(u => u.dictValue == value)
+  return unit ? unit.dictLabel : ''
+}
+
+const loadMyPublishInfo = (keyword) => {
+  isLoading.value = true
+  uni.showLoading({
+    title: '加载中'
+  })
   
-  computed: {
-    filteredPublishedProducts() {
-      // let products = this.publishedProductsList;
-      let products = this.productsList;
-      
-      // if (this.searchKeyword.trim()) {
-      //   const keyword = this.searchKeyword.trim().toLowerCase();
-      //   products = products.filter(item => 
-      //     item.name.toLowerCase().includes(keyword) || 
-      //     item.description.toLowerCase().includes(keyword)
-      //   );
-      // }
-      
-      return products;
-    },
-    
-    filteredPurchaseList() {
-      // let list = this.purchaseList;
-      let list = this.productsList;
-      
-      // if (this.purchaseSearchKeyword.trim()) {
-      //   const keyword = this.purchaseSearchKeyword.trim().toLowerCase();
-      //   list = list.filter(item => 
-      //     item.title.toLowerCase().includes(keyword) || 
-      //     item.category.toLowerCase().includes(keyword)
-      //   );
-      // }
-      
-      return list;
+  // 构建查询参数
+  const params = {
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+    createBy: currentUserInfo.username,
+    type: 0 // 默认查询出售信息
+  }
+  
+  if (keyword != null && keyword != '') {
+    params.searchKeyword = keyword
+  }
+  
+  // 分类逻辑
+  if (activeTab.value !== 'published') {
+    params.type = 1
+  }
+  
+  getProductInfoList(params).then(res => {
+    if (res.data.code === 200) {
+      const { rows, total } = res.data
+      if (pageNum.value === 1) {
+        productsList.value = rows
+      } else {
+        productsList.value = [...productsList.value, ...rows]
+      }
+      // 判断是否还有更多数据
+      noMoreData.value = productsList.value.length >= total
+      uni.hideLoading()
+    } else {
+      uni.showToast({
+        title: res.data.msg || '获取农品列表失败',
+        icon: 'none'
+      })
     }
-  },
-  onShow() {
-  	// console.log("my-publish",this.formData);
-  	this.loadMyPublishInfo()
-  },
-  // 页面加载时获取数据
-  onLoad() {
-  	this.loadMyPublishInfo();
-  },
-  methods: {
-	  // 上拉加载更多
-	  loadMore() {
-	  	if (!this.isLoading && !this.noMoreData) {
-	  		this.pageNum++;
-	  		this.loadMyPublishInfo();
-	  	}
-	  },
-	  getImageUrl(item) {
-	  	// 默认返回url或path
-	  	if (item == null) return
-	  	const imageUrls = item.split(',');
-	  	// console.log("imageUrls", imageUrls);
-	  	return imageUrls[0];
-	  },
-	  getUnitLabel(value) {
-	  	const unit = this.dictData.agricultural_unit.find(u => u.dictValue == value)
-	  	return unit ? unit.dictLabel : ''
-	  },
-	  loadMyPublishInfo(keyword) {
-	  	this.isLoading = true;
-	  	uni.showLoading({
-	  		title: '加载中'
-	  	});
-	  	// 构建查询参数
-	  	const params = {
-	  		pageNum: this.pageNum,
-	  		pageSize: this.pageSize,
-			createBy: this.currentUserInfo.username,
-			type: 0 // 默认查询出售信息
-	  	};
-	  	if (keyword != null && keyword != '') {
-	  		params.searchKeyword = keyword
-	  	}
-	  
-	  	// 分类逻辑
-	  	if (this.activeTab !== 'published' ) {
-	  		// 默认场景:推荐查询
-	  		params.type = 1;
-	  	}
-	  
-	  	getProductInfoList(params).then(res => {
-	  		if (res.data.code === 200) {
-	  			const {
-	  				rows,
-	  				total
-	  			} = res.data;
-	  			if (this.pageNum === 1) {
-	  				this.productsList = rows;
-	  			} else {
-	  				this.productsList = [...this.productsList, ...rows];
-	  			}
-	  			// 判断是否还有更多数据
-	  			this.noMoreData = this.productsList.length >= total;
-	  			uni.hideLoading();
-	  		} else {
-	  			uni.showToast({
-	  				title: res.data.msg || '获取农品列表失败',
-	  				icon: 'none'
-	  			});
-	  		}
-	  	}).catch(err => {
-	  		console.error('获取农品列表失败:', err);
-	  		uni.showToast({
-	  			title: '获取农品列表失败',
-	  			icon: 'none'
-	  		});
-	  	}).finally(() => {
-	  		this.isLoading = false;
-	  		this.isRefreshing = false;
-	  	});
-	  },
-    switchTab(tab) {
-      this.activeTab = tab;
-	  // this.publishedProductsList = [] // 切换时置空数据数组
-	  // this.purchaseList = [] 
-	  this.productsList = []
-	  // 滚动到当前分类位置
-	  this.loadMyPublishInfo(this.searchKeyword.trim())
-    },
-    
-    getStatusText(status) {
-      const statusMap = {
-        1: '审核中',
-        2: '已上架',
-        3: '已下架',
-		4: '未通过'
-      };
-      return statusMap[status] || '未知';
-    },
-	getStatusClassName(status) {
-	  const statusMapClassName = {
-	    1: 'pending',
-	    2: 'approved',
-	    3: 'rejected',
-		4: 'rejected'
-	  };
-	  return statusMapClassName[status] || '未知';
-	},
-    
-    handleSearch() {
-	  const keyword = this.searchKeyword.trim().toLowerCase();
-	  this.loadMyPublishInfo(keyword)
-    },
-    
-    navigateToDetail(product) {
-      uni.navigateTo({
-        url: `/pages/service/sales-detail?id=${product.id}&type=sale&name=${encodeURIComponent(product.title)}&source=myPublish&status=${product.status}`
-      });
-    },
-    
-    navigateToPurchaseDetail(item) {
-      uni.navigateTo({
-        url: `/pages/service/sales-detail?id=${item.id}&type=purchase&title=${encodeURIComponent(item.title)}&source=myPublish&status=${item.status}`
-      });
-    },
-    
-    navigateToPublish() {
-      this.showPublishModal = false;
-      uni.navigateTo({
-        url: '/pages/service/sales-publish'
-      });
-    },
-    
-    navigateToPurchasePublish() {
-      this.showPublishModal = false;
-      uni.navigateTo({
-        url: '/pages/service/purchase-publish'
-      });
-    },
+  }).catch(err => {
+    console.error('获取农品列表失败:', err)
+    uni.showToast({
+      title: '获取农品列表失败',
+      icon: 'none'
+    })
+  }).finally(() => {
+    isLoading.value = false
+    isRefreshing.value = false
+  })
+}
+
+const switchTab = (tab) => {
+  activeTab.value = tab
+  productsList.value = []
+  pageNum.value = 1
+  loadMyPublishInfo(searchKeyword.value.trim())
+}
+
+const getStatusText = (status) => {
+  const statusMap = {
+    1: '审核中',
+    2: '已上架',
+    3: '已下架',
+    4: '未通过'
+  }
+  return statusMap[status] || '未知'
+}
+
+const getStatusClassName = (status) => {
+  const statusMapClassName = {
+    1: 'pending',
+    2: 'approved',
+    3: 'rejected',
+    4: 'rejected'
   }
+  return statusMapClassName[status] || '未知'
+}
+
+const handleSearch = () => {
+  const keyword = searchKeyword.value.trim().toLowerCase()
+  pageNum.value = 1
+  productsList.value = []
+  loadMyPublishInfo(keyword)
+}
+
+const navigateToDetail = (product) => {
+  uni.navigateTo({
+    url: `/pages/service/sales-detail?id=${product.id}&type=sale&name=${encodeURIComponent(product.title)}&source=myPublish&status=${product.status}`
+  })
+}
+
+const navigateToPurchaseDetail = (item) => {
+  uni.navigateTo({
+    url: `/pages/service/sales-detail?id=${item.id}&type=purchase&title=${encodeURIComponent(item.title)}&source=myPublish&status=${item.status}`
+  })
+}
+
+const navigateToPublish = () => {
+  showPublishModal.value = false
+  uni.navigateTo({
+    url: '/pages/service/sales-publish'
+  })
+}
+
+const navigateToPurchasePublish = () => {
+  showPublishModal.value = false
+  uni.navigateTo({
+    url: '/pages/service/purchase-publish'
+  })
 }
+
+// 导出 onShow 供 uni-app 使用
+defineExpose({
+  onShow
+})
 </script>
 
 <style lang="scss">

+ 290 - 309
pages/service/purchase-publish.vue

@@ -188,153 +188,150 @@
 	</view>
 </template>
 
-<script>
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
 	import LocationPicker from "@/components/common/LocationPicker.vue"
-	import api from "@/config/api.js";
-	import storage from "@/utils/storage.js";
-	import {
-		addProductInfo,
-		getProductInfoById,
-		editProductInfo
-	} from '@/api/services/productInfo.js';
-	import dictMixin from '@/utils/mixins/dictMixin';
-	import {
-		getFormattedTime
-	} from '@/utils/dateUtils'
-	export default {
-		mixins: [dictMixin],
-		components: {
-			LocationPicker
-		},
-		data() {
-			return {
-				dictTypeList: ['agricultural_unit', 'agricultural_category'],
-				isEditMode: false, // 是否为编辑模式
-				editItemId: '', // 编辑的收购信息ID
-				formData: {
-					title: '',
-					categoryId: '',
-					price: '',
-					description: '',
-					imageUrl: '',
-					images: [],
-					unit: '',
-					quantity: '',
-					contactName: '',
-					contactPhone: '',
-					location: '',
-					publishTime: getFormattedTime(), // 发布时间
-					userId: storage.getUserInfo().userid,
-					type: 1, // 收购
-					status: 1, // 审核中
-				},
-				// categoryOptions: ['蔬菜', '水果', '粮食', '其他'],
-				dictDataOptions: [],
-				showCategoryPicker: false,
-				showUnitPicker: false,
-				titleLength: 0,
-				descLength: 0
-			}
-		},
-
-		onLoad(options) {
-			this.dictDataOptions = this.dictData
-			// 检查是否为编辑模式
-			if (options.action === 'edit' && options.id) {
-				this.isEditMode = true;
-				this.editItemId = options.id;
-
-				// 设置页面标题
-				uni.setNavigationBarTitle({
-					title: '编辑收购信息'
-				});
-
-				// 加载收购信息数据
-				this.loadPurchaseData();
+	import api from "@/config/api.js"
+	import storage from "@/utils/storage.js"
+	import { addProductInfo, getProductInfoById, editProductInfo } from '@/api/services/productInfo.js'
+	import { useDict } from '@/utils/composables/useDict'
+	import { getFormattedTime } from '@/utils/dateUtils'
+
+	// 使用 useDict 替换 dictMixin
+	const { dictData } = useDict(['agricultural_unit', 'agricultural_category'])
+
+	// 响应式状态
+	const isEditMode = ref(false)
+	const editItemId = ref('')
+	const showCategoryPicker = ref(false)
+	const showUnitPicker = ref(false)
+	const titleLength = ref(0)
+	const descLength = ref(0)
+
+	// 使用 reactive 管理复杂的表单对象
+	const formData = reactive({
+		title: '',
+		categoryId: '',
+		price: '',
+		description: '',
+		imageUrl: '',
+		images: [],
+		unit: '',
+		quantity: '',
+		contactName: '',
+		contactPhone: '',
+		location: '',
+		publishTime: getFormattedTime(),
+		userId: storage.getUserInfo().userid,
+		type: 1, // 收购
+		status: 1 // 审核中
+	})
+
+	// 计算属性:dictDataOptions
+	const dictDataOptions = computed(() => dictData.value)
+
+	// onLoad 替换为 onMounted + getCurrentPages
+	onMounted(() => {
+		const pages = getCurrentPages()
+		const currentPage = pages[pages.length - 1]
+		const options = currentPage.options
+
+		// 检查是否为编辑模式
+		if (options.action === 'edit' && options.id) {
+			isEditMode.value = true
+			editItemId.value = options.id
+
+			// 设置页面标题
+			uni.setNavigationBarTitle({
+				title: '编辑收购信息'
+			})
+
+			// 加载收购信息数据
+			loadPurchaseData()
+		} else {
+			// 新建模式
+			isEditMode.value = false
+			uni.setNavigationBarTitle({
+				title: '发布收购信息'
+			})
+		}
+	})
+
+	// 方法定义
+	// 加载收购信息数据(编辑模式)
+	const loadPurchaseData = () => {
+		uni.showLoading({
+			title: '加载中...'
+		})
+
+		getProductInfoById(editItemId.value).then(res => {
+			if (res.data.code === 200) {
+				Object.assign(formData, res.data.data)
+				// 更新字符计数
+				titleLength.value = formData.title.length
+				descLength.value = formData.description.length
+				
+				console.log("this.goodsDetail", formData)
+				// 处理图片数据
+				if (formData.imageUrl) {
+					try {
+						formData.images = formData.imageUrl.split(',').map(url => ({ url, status: 'success' }))
+						console.log('解析后的图片数据:', formData.images)
+					} catch (e) {
+						console.error('解析图片数据失败:', e)
+						formData.images = []
+					}
+				} else {
+					formData.images = []
+				}
+				uni.hideLoading()
 			} else {
-				// 新建模式
-				this.isEditMode = false;
-				uni.setNavigationBarTitle({
-					title: '发布收购信息'
-				});
-			}
-		},
-
-		methods: {
-			// 加载收购信息数据(编辑模式)
-			loadPurchaseData() {
-				// 模拟从API加载数据,实际应用中需要根据ID调用相应的API
-				uni.showLoading({
-					title: '加载中...'
-				});
-
-				getProductInfoById(this.editItemId).then(res=>{
-					if (res.data.code === 200) {
-						  // const { data } = res.data;
-						 // this.formData = data
-						 Object.assign(this.formData, res.data.data)  // 保持响应式
-						 // 更新字符计数
-						 this.nameLength = this.formData.title.length;
-						 this.descLength = this.formData.description.length;
-						 
-						  console.log("this.goodsDetail", this.formData);
-						  // 处理图片数据
-						  if (this.formData.imageUrl) {
-						    try {
-							   this.formData.images = this.formData.imageUrl.split(',').map(url => ({ url,status: 'success' }))
-						      console.log('解析后的图片数据:', this.formData.images);
-						    } catch (e) {
-						      console.error('解析图片数据失败:', e);
-						      this.formData.images = [];
-								}
-						  } else {
-						    this.formData.images = [];
-						  }
-						  uni.hideLoading();
-						} else {
-						  uni.showToast({
-						    title: res.data.msg || '获取农品信息失败',
-						    icon: 'none'
-						  });
-						}
+				uni.showToast({
+					title: res.data.msg || '获取农品信息失败',
+					icon: 'none'
 				})
-			},
-
-			// 标题输入处理
-			onTitleInput(e) {
-				this.titleLength = e.detail.value.length;
-			},
-
-			// 描述输入处理
-			onDescInput(e) {
-				this.descLength = e.detail.value.length;
-			},
-
-			// 选择分类
-			selectCategory(category) {
-				this.formData.categoryId = category.dictValue;
-				this.formData.categoryLabel = category.dictLabel;
-				this.showCategoryPicker = false;
-			},
-			// 选择单位
-			selectUnit(unit) {
-				this.formData.unit = unit.dictValue;
-				this.formData.dictLabel = unit.dictLabel;
-				this.showUnitPicker = false;
-			},
-			getDictLabel(dictKey, value) {
-			   if (!this.dictData || !this.dictData[dictKey]) {
-			     return ''
-			   }
-			   const list = this.dictData[dictKey] || []
-			   const item = list.find(u => u.dictValue == value)
-			   return item ? item.dictLabel : ''
-			 },
-
-			// 选择图片
-			chooseImage() {
-			  uni.chooseImage({
-			    count: 6 - this.formData.images.length,
+			}
+		})
+	}
+
+	// 标题输入处理
+	const onTitleInput = (e) => {
+		titleLength.value = e.detail.value.length
+	}
+
+	// 描述输入处理
+	const onDescInput = (e) => {
+		descLength.value = e.detail.value.length
+	}
+
+	// 选择分类
+	const selectCategory = (category) => {
+		formData.categoryId = category.dictValue
+		formData.categoryLabel = category.dictLabel
+		showCategoryPicker.value = false
+	}
+
+	// 选择单位
+	const selectUnit = (unit) => {
+		formData.unit = unit.dictValue
+		formData.dictLabel = unit.dictLabel
+		showUnitPicker.value = false
+	}
+
+	// 获取字典标签
+	const getDictLabel = (dictKey, value) => {
+		if (!dictData.value || !dictData.value[dictKey]) {
+			return ''
+		}
+		const list = dictData.value[dictKey] || []
+		const item = list.find(u => u.dictValue == value)
+		return item ? item.dictLabel : ''
+	}
+
+	// 选择图片
+	const chooseImage = () => {
+		uni.chooseImage({
+			count: 6 - formData.images.length,
 			    sizeType: ['original', 'compressed'],
 			    sourceType: ['album', 'camera'],
 			    success: (res) => {
@@ -374,21 +371,21 @@
 			      
 			      // 如果有有效文件,则上传
 			      if (validFiles.length > 0) {
-			        this.uploadImages(validFiles);
+			        uploadImages(validFiles)
 			      }
 			    },
 			    fail: (err) => {
-			      console.error('选择图片失败:', err);
+			      console.error('选择图片失败:', err)
 			      uni.showToast({
 			        title: '选择图片失败',
 			        icon: 'none'
-			      });
+			      })
 			    }
-			  });
-			},
-			
-			// 上传图片到服务器
-			uploadImages(tempFilePaths) {
+		})
+	}
+	
+	// 上传图片到服务器
+	const uploadImages = (tempFilePaths) => {
 			  uni.showLoading({
 			    title: '上传中...',
 			    mask: true
@@ -461,192 +458,176 @@
 			        if (successCount + failCount === totalFiles) {
 			          if (newImages.length > 0) {
 			            // 将新上传的图片添加到已有图片列表
-			            this.formData.images = [...this.formData.images, ...newImages];
+			            formData.images = [...formData.images, ...newImages]
 			            // 更新taskImages字段,将图片URL用逗号连接
-			           this.formData.imageUrl = this.formData.images.map(item => item.url).join(',');
-					   this.$set(this.formData, 'images', this.formData.images)  // 确保视图刷新
+			            formData.imageUrl = formData.images.map(item => item.url).join(',')
 			            // 显示成功提示
-			            uni.hideLoading();
+			            uni.hideLoading()
 			            uni.showToast({
 			              title: `成功上传${successCount}张图片`,
 			              icon: 'success'
-			            });
+			            })
 			          } else {
 			            // 全部失败
 						uni.showToast({
 						  title: `图片上传: ${newImages}`,
-						  icon: 'none',
-						});
-			            uni.hideLoading();
-			            // uni.showToast({
-			            //   title: '图片上传失败',
-			            //   icon: 'none',
-			            // });
+						  icon: 'none'
+						})
+			            uni.hideLoading()
 			          }
 			        }
 			      }
-			    });
-			  });
-			},
-			
-			removeImage(index) {
-			  uni.showModal({
-			    title: '确认删除',
-			    content: '确定要删除这张图片吗?',
-			    success: (res) => {
-			      if (res.confirm) {
-			        this.formData.images.splice(index, 1);
-			        
-			        // 更新taskImages字段
-			       this.formData.imageUrl = this.formData.images.map(item => item.url).join(',');
-				   this.$set(this.formData, 'images', this.formData.images)  // 确保视图刷新
-			        uni.showToast({
-			          title: '已删除',
-			          icon: 'none'
-			        });
-			      }
-			    }
-			  });
-			},
-
-			// 表单验证
-			validateForm() {
-				if (!this.formData.title.trim()) {
+			    })
+			  })
+	}
+	
+	const removeImage = (index) => {
+		uni.showModal({
+			title: '确认删除',
+			content: '确定要删除这张图片吗?',
+			success: (res) => {
+				if (res.confirm) {
+					formData.images.splice(index, 1)
+					
+					// 更新taskImages字段
+					formData.imageUrl = formData.images.map(item => item.url).join(',')
 					uni.showToast({
-						title: '请输入收购标题',
+						title: '已删除',
 						icon: 'none'
-					});
-					return false;
+					})
 				}
+			}
+		})
+	}
 
-				if (!this.formData.categoryId) {
-					uni.showToast({
-						title: '请选择收购品类',
-						icon: 'none'
-					});
-					return false;
-				}
+	// 表单验证
+	const validateForm = () => {
+		if (!formData.title.trim()) {
+			uni.showToast({
+				title: '请输入收购标题',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (!this.formData.quantity || this.formData.quantity <= 0) {
-					uni.showToast({
-						title: '请输入有效的收购数量',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.categoryId) {
+			uni.showToast({
+				title: '请选择收购品类',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (!this.formData.price || this.formData.price <= 0) {
-					uni.showToast({
-						title: '请输入有效的单价预算',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.quantity || formData.quantity <= 0) {
+			uni.showToast({
+				title: '请输入有效的收购数量',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (!this.formData.contactName.trim()) {
-					uni.showToast({
-						title: '请输入联系人姓名',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.price || formData.price <= 0) {
+			uni.showToast({
+				title: '请输入有效的单价预算',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (!this.formData.contactPhone.trim()) {
-					uni.showToast({
-						title: '请输入联系电话',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.contactName.trim()) {
+			uni.showToast({
+				title: '请输入联系人姓名',
+				icon: 'none'
+			})
+			return false
+		}
 
-				// 简单的手机号格式验证
-				const phoneRegex = /^1[3-9]\d{9}$/;
-				if (!phoneRegex.test(this.formData.contactPhone)) {
-					uni.showToast({
-						title: '请输入正确的手机号码',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.contactPhone.trim()) {
+			uni.showToast({
+				title: '请输入联系电话',
+				icon: 'none'
+			})
+			return false
+		}
 
-				return true;
-			},
+		// 简单的手机号格式验证
+		const phoneRegex = /^1[3-9]\d{9}$/
+		if (!phoneRegex.test(formData.contactPhone)) {
+			uni.showToast({
+				title: '请输入正确的手机号码',
+				icon: 'none'
+			})
+			return false
+		}
 
-			// 提交表单
-			submitForm() {
-				if (!this.validateForm()) {
-					return;
-				}
+		return true
+	}
 
-				// 显示加载提示
-				const loadingTitle = this.isEditMode ? '保存中...' : '提交中...';
-				uni.showLoading({
-					title: loadingTitle
-				});
+	// 提交表单
+	const submitForm = () => {
+		if (!validateForm()) {
+			return
+		}
 
-				uni.hideLoading();
-				
-				if (this.isEditMode) {
-					editProductInfo(this.formData).then(res=>{
-						if(res.data.code === 200){
-							uni.showModal({
-								title: '保存成功',
-								content: '您的修改已保存成功!',
-								showCancel: false,
-								success: () => {
-									uni.navigateBack();
-								}
-							});
-						}else{
-							uni.showToast({
-								title: res.data.msg || '提交失败,请稍后重试',
-								icon: 'none'
-							});
+		// 显示加载提示
+		const loadingTitle = isEditMode.value ? '保存中...' : '提交中...'
+		uni.showLoading({
+			title: loadingTitle
+		})
+
+		uni.hideLoading()
+		
+		if (isEditMode.value) {
+			editProductInfo(formData).then(res => {
+				if (res.data.code === 200) {
+					uni.showModal({
+						title: '保存成功',
+						content: '您的修改已保存成功!',
+						showCancel: false,
+						success: () => {
+							uni.navigateBack()
 						}
-					}).catch(err => {
-						console.error("提交异常:", err);
-						uni.showToast({
-							title: '网络错误,请检查后重试',
-							icon: 'none'
-						});
-					});
-					
+					})
 				} else {
-					// 新建模式 - 提交审核
-					console.log("this.formData",this.formData);
-					// const data = {
-					// 	userId: this.currentUserInfo.userid,
-					// 	type: 0 ,// 出售
-					// 	status: 1 ,// 审核中
-					// }
-					// this.formData = {...this.formData , ...data}
-					addProductInfo(this.formData).then(res=>{
-						console.log("新增出售农产品",res);
-						if(res.data.code === 200){
-							uni.showModal({
-								title: '提交成功',
-								content: '您的收购农产品信息已提交审核,审核通过后将在收购页面展示。',
-								showCancel: false,
-								success: () => {
-									uni.navigateBack();
-								}
-							});
-						}else{
-							uni.showToast({
-								title: res.data.msg || '提交失败,请稍后重试',
-								icon: 'none'
-							});
+					uni.showToast({
+						title: res.data.msg || '提交失败,请稍后重试',
+						icon: 'none'
+					})
+				}
+			}).catch(err => {
+				console.error("提交异常:", err)
+				uni.showToast({
+					title: '网络错误,请检查后重试',
+					icon: 'none'
+				})
+			})
+		} else {
+			// 新建模式 - 提交审核
+			console.log("this.formData", formData)
+			addProductInfo(formData).then(res => {
+				console.log("新增出售农产品", res)
+				if (res.data.code === 200) {
+					uni.showModal({
+						title: '提交成功',
+						content: '您的收购农产品信息已提交审核,审核通过后将在收购页面展示。',
+						showCancel: false,
+						success: () => {
+							uni.navigateBack()
 						}
-					}).catch(err => {
-						console.error("提交异常:", err);
-						uni.showToast({
-							title: '网络错误,请检查后重试',
-							icon: 'none'
-						});
-					});
-					
+					})
+				} else {
+					uni.showToast({
+						title: res.data.msg || '提交失败,请稍后重试',
+						icon: 'none'
+					})
 				}
-			}
+			}).catch(err => {
+				console.error("提交异常:", err)
+				uni.showToast({
+					title: '网络错误,请检查后重试',
+					icon: 'none'
+				})
+			})
 		}
 	}
 </script>

+ 296 - 310
pages/service/sales-detail.vue

@@ -171,326 +171,312 @@
   </view>
 </template>
 
-<script>
-	import LocationPicker from "@/components/common/LocationPicker.vue"
-	import {
-		getProductInfoById,editProductInfo
-	} from '@/api/services/productInfo.js';
-	import dictMixin from '@/utils/mixins/dictMixin';
-export default {
-	mixins: [dictMixin],
-	components: { LocationPicker },
-  data() {
-    return {
-	  dictTypeList: ['agricultural_category','agricultural_unit'],
-      currentImageIndex: 0,
-      source: '', // 页面来源,myPublish表示来自我的发布页面
-      productStatus: '', // 产品状态:1-pending, 2-approved, 3-rejected
-      productInfo: {
-        id: '',
-        type: '0', // 0-sale: 出售, 1-purchase: 收购
-        title: '',
-        categoryId: '',
-        price: '',
-        quantity: '',
-        unit: '',
-        location: '',
-        description: '',
-        contactName:'',
-		contactPhone:'',
-        publishTime: '',
-        images: [],
-		remark: ''
-      },
-      imageList: [],
-      isOwnProduct: false ,// 是否为当前用户发布的产品
-	  // 状态按钮映射
-	      actionMap: {
-	        '1': [ // 审核中
-	          // { label: '编辑', class: 'edit-btn', action: 'editProduct' }, 
-	          // { label: '撤销', class: 'cancel-btn', action: 'cancelProduct' }
-	        ],
-	        '2': [ // 已上架
-	          // { label: '编辑', class: 'edit-btn', action: 'editProduct' }, 
-	          { label: '下架', class: 'remove-btn', action: 'removeProduct' }
-	        ],
-	        '3': [ // 已下架
-	          { label: '编辑', class: 'edit-btn', action: 'editProduct' },
-	          { label: '发布', class: 'cancel-btn', action: 'publishProduct' }
-	        ],
-	        '4': [ // 未通过
-	          { label: '编辑', class: 'edit-btn', action: 'editProduct' },
-	          // { label: '删除', class: 'cancel-btn', action: 'cancelProduct' }
-	        ]
-	      }
-    }
-  },
-  onShow() {
-  	console.log("onshow",this.productInfo);
-	this.loadProductDetail(this.productInfo.id)
-  },
-  onLoad(options) {
-    // 获取页面参数
-    if (options.id) {
-      this.source = options.source || '';
-      this.productStatus = options.status || '';
-      this.loadProductDetail(options.id, options.type);
-    }
-  },
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import LocationPicker from "@/components/common/LocationPicker.vue"
+import { getProductInfoById, editProductInfo } from '@/api/services/productInfo.js'
+import { useDict } from '@/utils/composables/useDict'
+
+// 使用字典
+const { dictData } = useDict(['agricultural_category', 'agricultural_unit'])
+
+const currentImageIndex = ref(0)
+const source = ref('') // 页面来源,myPublish表示来自我的发布页面
+const productStatus = ref('') // 产品状态:1-pending, 2-approved, 3-rejected
+const productInfo = ref({
+  id: '',
+  type: '0', // 0-sale: 出售, 1-purchase: 收购
+  title: '',
+  categoryId: '',
+  price: '',
+  quantity: '',
+  unit: '',
+  location: '',
+  description: '',
+  contactName: '',
+  contactPhone: '',
+  publishTime: '',
+  images: [],
+  remark: ''
+})
+const imageList = ref([])
+const isOwnProduct = ref(false) // 是否为当前用户发布的产品
+
+// 状态按钮映射
+const actionMap = {
+  '1': [], // 审核中
+  '2': [ // 已上架
+    { label: '下架', class: 'remove-btn', action: 'removeProduct' }
+  ],
+  '3': [ // 已下架
+    { label: '编辑', class: 'edit-btn', action: 'editProduct' },
+    { label: '发布', class: 'cancel-btn', action: 'publishProduct' }
+  ],
+  '4': [ // 未通过
+    { label: '编辑', class: 'edit-btn', action: 'editProduct' }
+  ]
+}
+
+// 判断是否显示操作按钮
+const shouldShowActionButtons = computed(() => {
+  if (!isOwnProduct.value) {
+    // 他人发布的内容始终显示联系按钮
+    return true
+  } else {
+    // 自己发布的内容,已下架状态不显示任何按钮
+    return productStatus.value !== 'rejected'
+  }
+})
+
+// 页面显示时刷新数据
+const onShow = () => {
+  if (productInfo.value.id) {
+    loadProductDetail(productInfo.value.id)
+  }
+}
+
+// 页面加载
+onMounted(() => {
+  const pages = getCurrentPages()
+  const currentPage = pages[pages.length - 1]
+  const options = currentPage.options
+  
+  if (options.id) {
+    source.value = options.source || ''
+    productStatus.value = options.status || ''
+    loadProductDetail(options.id, options.type)
+  }
+})
+
+const handleAction = (action) => {
+  const actions = {
+    editProduct,
+    removeProduct,
+    publishProduct,
+    cancelProduct
+  }
+  actions[action] && actions[action]()
+}
+
+const getDictLabel = (dictKey, value) => {
+  if (!dictData.value || !dictData.value[dictKey]) {
+    return ''
+  }
+  const list = dictData.value[dictKey] || []
+  const item = list.find(u => u.dictValue == value)
+  return item ? item.dictLabel : ''
+}
+
+// 加载产品详情
+const loadProductDetail = (id) => {
+  uni.showLoading({
+    title: '加载中'
+  })
   
-  computed: {
-    // 判断是否显示操作按钮
-    shouldShowActionButtons() {
-      if (!this.isOwnProduct) {
-        // 他人发布的内容始终显示联系按钮
-        return true;
+  getProductInfoById(id).then(res => {
+    if (res.data.code === 200) {
+      const { data } = res.data
+      productInfo.value = data
+      
+      // 处理图片数据
+      if (productInfo.value.imageUrl) {
+        try {
+          imageList.value = productInfo.value.imageUrl.split(',')
+        } catch (e) {
+          console.error('解析图片数据失败:', e)
+          imageList.value = []
+        }
       } else {
-        // 自己发布的内容,已下架状态不显示任何按钮
-        return this.productStatus !== 'rejected';
+        imageList.value = []
       }
+      uni.hideLoading()
+    } else {
+      uni.showToast({
+        title: res.data.msg || '获取农品信息失败',
+        icon: 'none'
+      })
     }
-  },
+  })
   
-  methods: {
-	// getUnitLabel(value) {
-	// 	console.log("this.dictData",this.dictData);
-	//   	const unit = this.dictData.agricultural_category.find(u => u.dictValue == value)
-	//   	return unit ? unit.dictLabel : ''
-	// },
-	 handleAction(action) {
-	    this[action] && this[action](); // 动态调用 editProduct/removeProduct 等方法
-	  },
-	 getDictLabel(dictKey, value) {
-	    if (!this.dictData || !this.dictData[dictKey]) {
-	      return ''
-	    }
-	    const list = this.dictData[dictKey] || []
-	    const item = list.find(u => u.dictValue == value)
-	    return item ? item.dictLabel : ''
-	  },
-    // 加载产品详情
-    loadProductDetail(id) {
-      // 根据类型模拟不同的数据结构
-      
-      uni.showLoading({
-      	title: '加载中'
-      });
-      getProductInfoById(id).then(res=>{
-      	if (res.data.code === 200) {
-      		  const { data } = res.data;
-      		 this.productInfo = data
-      		  console.log("this.goodsDetail", this.productInfo);
-      		  // 处理图片数据
-      		  if (this.productInfo.imageUrl) {
-      		    try {
-      			  this.imageList = this.productInfo.imageUrl.split(',')
-      		      console.log('解析后的图片数据:', this.imageList);
-      		    } catch (e) {
-      		      console.error('解析图片数据失败:', e);
-      		      this.imageList = [];
-				}
-      		  } else {
-      		    this.imageList = [];
-      		  }
-      		  uni.hideLoading();
-      		} else {
-      		  uni.showToast({
-      		    title: res.data.msg || '获取农品信息失败',
-      		    icon: 'none'
-      		  });
-      		}
-	  })
-     
-      // this.imageList = mockData.images;
-      
-      // 判断是否为当前用户发布的产品
-      // 如果来源是我的发布页面,则表示是自己的产品
-      this.isOwnProduct = this.source === 'myPublish';
-    },
-    
-    // 轮播图切换
-    onSwiperChange(e) {
-      this.currentImageIndex = e.detail.current;
-    },
-    
-    // 预览图片
-    previewImage(index) {
-      uni.previewImage({
-        current: index,
-        urls: this.imageList
-      });
-    },
-    
-    // 编辑产品
-    editProduct() {
-      if (this.productInfo.type === 1) {
-        // 收购信息跳转到收购编辑页面
-        uni.navigateTo({
-          url: `/pages/service/purchase-publish?action=edit&id=${this.productInfo.id}`
-        });
-      } else {
-        // 销售信息跳转到销售编辑页面
-        uni.navigateTo({
-          url: `/pages/service/sales-publish?action=edit&id=${this.productInfo.id}&type=${this.productInfo.type}`
-        });
+  // 判断是否为当前用户发布的产品
+  isOwnProduct.value = source.value === 'myPublish'
+}
+
+// 轮播图切换
+const onSwiperChange = (e) => {
+  currentImageIndex.value = e.detail.current
+}
+
+// 预览图片
+const previewImage = (index) => {
+  uni.previewImage({
+    current: index,
+    urls: imageList.value
+  })
+}
+
+// 编辑产品
+const editProduct = () => {
+  if (productInfo.value.type === 1) {
+    // 收购信息跳转到收购编辑页面
+    uni.navigateTo({
+      url: `/pages/service/purchase-publish?action=edit&id=${productInfo.value.id}`
+    })
+  } else {
+    // 销售信息跳转到销售编辑页面
+    uni.navigateTo({
+      url: `/pages/service/sales-publish?action=edit&id=${productInfo.value.id}&type=${productInfo.value.type}`
+    })
+  }
+}
+
+const publishProduct = () => {
+  uni.showModal({
+    title: '确认发布审核',
+    content: `确定要发布这条信息吗?审核通过后将在${productInfo.value.type === 0 ? '销售' : '收购'}页面展示`,
+    confirmText: '确认发布',
+    cancelText: '取消',
+    success: (res) => {
+      if (res.confirm) {
+        handlePublishProduct()
       }
-    },
-    
-	publishProduct(){
-		uni.showModal({
-		  title: '确认发布审核',
-		  content: `确定要发布这条信息吗?审核通过后将在${this.productInfo.type === 0 ? '销售' : '收购'}页面展示`,
-		  confirmText: '确认发布',
-		  cancelText: '取消',
-		  success: (res) => {
-		    if (res.confirm) {
-		      // 执行下架操作
-		      this.handlePublishProduct();
-		    }
-		  }
-		});
-	},
-	
-	// 处理发布操作
-	handlePublishProduct() {
-	  // 模拟下架API调用
-	  uni.showLoading({ title: '发布中...' });
-	  const data = {
-		  id: this.productInfo.id,
-		  status: 1 // 审核中
-	  }
-	  editProductInfo(data).then(res=>{
-		  uni.hideLoading();
-		  if(res.data.code === 200){
-			  uni.showToast({
-			    title: '发布成功',
-			    icon: 'success'
-			  });
-			// 返回上一页
-			setTimeout(() => {
-			  uni.navigateBack();
-			}, 1000);
-		  }else{
-			uni.showToast({
-				title: res.data.msg || '发布失败,请稍后重试',
-				icon: 'none'
-			});
-		}
-	  })
-	},
-	
-    // 下架产品
-    removeProduct() {
-      uni.showModal({
-        title: '确认下架',
-        content: '确定要下架这条信息吗?下架后其他用户将无法查看。',
-        confirmText: '确认下架',
-        cancelText: '取消',
-        success: (res) => {
-          if (res.confirm) {
-            // 执行下架操作
-            this.handleRemoveProduct();
-          }
-        }
-      });
-    },
-    
-    // 处理下架操作
-    handleRemoveProduct() {
-      // 模拟下架API调用
-      uni.showLoading({ title: '下架中...' });
-	  const data = {
-		  id: this.productInfo.id,
-		  status: 3 // 下架
-	  }
-      editProductInfo(data).then(res=>{
-		  uni.hideLoading();
-		  if(res.data.code === 200){
-			  uni.showToast({
-			    title: '下架成功',
-			    icon: 'success'
-			  });
-			// 返回上一页
-			setTimeout(() => {
-			  uni.navigateBack();
-			}, 1500);
-		  }else{
-			uni.showToast({
-				title: res.data.msg || '下架失败,请稍后重试',
-				icon: 'none'
-			});
-		}
-	  })
-    },
-    
-    // 联系发布者
-    contactPublisher() {
-      // 获取真实电话号码并拨打
-      const realPhone = this.getRealPhoneNumber();
-      
-      uni.showModal({
-        title: '联系发布者',
-        content: `确定要拨打 ${realPhone} 吗?`,
-        confirmText: '拨打',
-        cancelText: '取消',
-        success: (res) => {
-          if (res.confirm) {
-            uni.makePhoneCall({
-              phoneNumber: realPhone,
-              fail: () => {
-                uni.showToast({
-                  title: '拨号失败',
-                  icon: 'none'
-                });
-              }
-            });
-          }
-        }
-      });
-    },
-    
-    // 撤销产品(审核中状态)
-    cancelProduct() {
-      uni.showModal({
-        title: '确认撤销',
-        content: '确定要撤销这条发布信息吗?撤销后需要重新提交审核。',
-        confirmText: '确认撤销',
-        cancelText: '取消',
-        success: (res) => {
-          if (res.confirm) {
-            // 执行撤销操作
-            this.handleCancelProduct();
-          }
-        }
-      });
-    },
-    
-    // 处理撤销操作
-    handleCancelProduct() {
-      // 模拟撤销API调用
-      uni.showLoading({ title: '撤销中...' });
-      
+    }
+  })
+}
+
+// 处理发布操作
+const handlePublishProduct = () => {
+  uni.showLoading({ title: '发布中...' })
+  const data = {
+    id: productInfo.value.id,
+    status: 1 // 审核中
+  }
+  editProductInfo(data).then(res => {
+    uni.hideLoading()
+    if (res.data.code === 200) {
+      uni.showToast({
+        title: '发布成功',
+        icon: 'success'
+      })
       setTimeout(() => {
-        uni.hideLoading();
-        uni.showToast({
-          title: '撤销成功',
-          icon: 'success'
-        });
-        
-        // 返回上一页
-        setTimeout(() => {
-          uni.navigateBack();
-        }, 1500);
-      }, 1000);
-    },
-    
-    // 获取真实电话号码(实际应用中从API获取)
-    getRealPhoneNumber() {
-      // 模拟从API获取真实电话号码
-      return '18812341234';
+        uni.navigateBack()
+      }, 1000)
+    } else {
+      uni.showToast({
+        title: res.data.msg || '发布失败,请稍后重试',
+        icon: 'none'
+      })
+    }
+  })
+}
+
+// 下架产品
+const removeProduct = () => {
+  uni.showModal({
+    title: '确认下架',
+    content: '确定要下架这条信息吗?下架后其他用户将无法查看。',
+    confirmText: '确认下架',
+    cancelText: '取消',
+    success: (res) => {
+      if (res.confirm) {
+        handleRemoveProduct()
+      }
     }
+  })
+}
+
+// 处理下架操作
+const handleRemoveProduct = () => {
+  uni.showLoading({ title: '下架中...' })
+  const data = {
+    id: productInfo.value.id,
+    status: 3 // 下架
   }
+  editProductInfo(data).then(res => {
+    uni.hideLoading()
+    if (res.data.code === 200) {
+      uni.showToast({
+        title: '下架成功',
+        icon: 'success'
+      })
+      setTimeout(() => {
+        uni.navigateBack()
+      }, 1500)
+    } else {
+      uni.showToast({
+        title: res.data.msg || '下架失败,请稍后重试',
+        icon: 'none'
+      })
+    }
+  })
 }
+
+// 联系发布者
+const contactPublisher = () => {
+  const realPhone = getRealPhoneNumber()
+  
+  uni.showModal({
+    title: '联系发布者',
+    content: `确定要拨打 ${realPhone} 吗?`,
+    confirmText: '拨打',
+    cancelText: '取消',
+    success: (res) => {
+      if (res.confirm) {
+        uni.makePhoneCall({
+          phoneNumber: realPhone,
+          fail: () => {
+            uni.showToast({
+              title: '拨号失败',
+              icon: 'none'
+            })
+          }
+        })
+      }
+    }
+  })
+}
+
+// 撤销产品(审核中状态)
+const cancelProduct = () => {
+  uni.showModal({
+    title: '确认撤销',
+    content: '确定要撤销这条发布信息吗?撤销后需要重新提交审核。',
+    confirmText: '确认撤销',
+    cancelText: '取消',
+    success: (res) => {
+      if (res.confirm) {
+        handleCancelProduct()
+      }
+    }
+  })
+}
+
+// 处理撤销操作
+const handleCancelProduct = () => {
+  uni.showLoading({ title: '撤销中...' })
+  
+  setTimeout(() => {
+    uni.hideLoading()
+    uni.showToast({
+      title: '撤销成功',
+      icon: 'success'
+    })
+    
+    setTimeout(() => {
+      uni.navigateBack()
+    }, 1500)
+  }, 1000)
+}
+
+// 获取真实电话号码
+const getRealPhoneNumber = () => {
+  return '18812341234'
+}
+
+// 导出 onShow 供 uni-app 使用
+defineExpose({
+  onShow
+})
 </script>
 
 <style lang="scss">

+ 308 - 324
pages/service/sales-publish.vue

@@ -210,159 +210,159 @@
 	</view>
 </template>
 
-<script>
+<script setup>
+	import { ref, reactive, computed, onMounted } from 'vue'
 	import LocationPicker from "@/components/common/LocationPicker.vue"
-	import api from "@/config/api.js";
-	import storage from "@/utils/storage.js";
-	import { addProductInfo ,getProductInfoById,editProductInfo} from '@/api/services/productInfo.js';
-	import dictMixin from '@/utils/mixins/dictMixin';
-	import {
-		getFormattedTime
-	} from '@/utils/dateUtils'
-	export default {
-		mixins: [dictMixin],
-		components: { LocationPicker },
-		data() {
-			return {
-				dictTypeList: ['agricultural_unit','agricultural_category'],
-				isEditMode: false, // 是否为编辑模式
-				editProductId: '', // 编辑的产品ID
-				productType: '0', // 产品类型:0-sale(销售)或 1-purchase(收购)
-				formData: {
-					title: '',
-					categoryId: '',
-					price: '',
-					description: '',
-					imageUrl: '',
-					images: [],
-					unit:'',
-					quantity: '',
-					contactName: '',
-					contactPhone: '',
-					location: '',
-					publishTime: getFormattedTime(), // 发布时间
-					userId: storage.getUserInfo().userid,
-					type: 0 ,// 出售
-					status: 1 ,// 审核中
-				},
-				locationInfo: {
-					address: '张家村',
-					field: '水稻田A区'
-				},
-				currentUserInfo: storage.getUserInfo(),
-				// categoryOptions: ['蔬菜', '水果', '粮食', '畜产品', '水产品', '中药材'],
-				dictDataOptions: [],
-				showCategoryPicker: false,
-				showUnitPicker: false,
-				nameLength: 0,
-				descLength: 0,
-				// localData: [], //省市区地址
-			}
-		},
+	import api from "@/config/api.js"
+	import storage from "@/utils/storage.js"
+	import { addProductInfo, getProductInfoById, editProductInfo } from '@/api/services/productInfo.js'
+	import { useDict } from '@/utils/composables/useDict'
+	import { getFormattedTime } from '@/utils/dateUtils'
+
+	// 使用 useDict 替换 dictMixin
+	const { dictData } = useDict(['agricultural_unit', 'agricultural_category'])
+
+	// 响应式状态
+	const isEditMode = ref(false)
+	const editProductId = ref('')
+	const productType = ref('0')
+	const showCategoryPicker = ref(false)
+	const showUnitPicker = ref(false)
+	const nameLength = ref(0)
+	const descLength = ref(0)
+
+	// 使用 reactive 管理复杂的表单对象
+	const formData = reactive({
+		title: '',
+		categoryId: '',
+		price: '',
+		description: '',
+		imageUrl: '',
+		images: [],
+		unit: '',
+		quantity: '',
+		contactName: '',
+		contactPhone: '',
+		location: '',
+		publishTime: getFormattedTime(),
+		userId: storage.getUserInfo().userid,
+		type: 0, // 出售
+		status: 1 // 审核中
+	})
+
+	const locationInfo = reactive({
+		address: '张家村',
+		field: '水稻田A区'
+	})
+
+	const currentUserInfo = storage.getUserInfo()
+
+	// 计算属性:dictDataOptions
+	const dictDataOptions = computed(() => dictData.value)
+
+	// onLoad 替换为 onMounted + getCurrentPages
+	onMounted(() => {
+		const pages = getCurrentPages()
+		const currentPage = pages[pages.length - 1]
+		const options = currentPage.options
+
+		// 检查是否为编辑模式
+		if (options.action === 'edit' && options.id) {
+			isEditMode.value = true
+			editProductId.value = options.id
+			productType.value = options.type || '0'
+
+			// 设置页面标题
+			uni.setNavigationBarTitle({
+				title: productType.value === '0' ? '编辑销售信息' : '编辑收购信息'
+			})
+
+			// 加载产品数据
+			loadProductData()
+		} else {
+			// 新建模式
+			isEditMode.value = false
+			uni.setNavigationBarTitle({
+				title: '发布农产品'
+			})
+		}
+	})
+
+	// 方法定义
+	// 加载产品数据(编辑模式)
+	const loadProductData = () => {
+		uni.showLoading({
+			title: '加载中...'
+		})
 		
-		onLoad(options) {
-
-			this.dictDataOptions = this.dictData
-			
-			console.log("this.dictDataOptions",this.dictDataOptions);
-			// 检查是否为编辑模式
-			if (options.action === 'edit' && options.id) {
-				this.isEditMode = true;
-				this.editProductId = options.id;
-				this.productType = options.type || '0';
-
-				// 设置页面标题
-				uni.setNavigationBarTitle({
-					title: this.productType === '0' ? '编辑销售信息' : '编辑收购信息'
-				});
-
-				// 加载产品数据
-				this.loadProductData();
-			} else {
-				// 新建模式
-				this.isEditMode = false;
-				uni.setNavigationBarTitle({
-					title: '发布农产品'
-				});
-			}
-		},
-
-		methods: {
-			// 加载产品数据(编辑模式)
-			loadProductData() {
-				// 模拟从API加载数据,实际应用中需要根据ID和类型调用相应的API
-				uni.showLoading({
-					title: '加载中...'
-				});
+		getProductInfoById(editProductId.value).then(res => {
+			if (res.data.code === 200) {
+				Object.assign(formData, res.data.data)
+				// 更新字符计数
+				nameLength.value = formData.title.length
+				descLength.value = formData.description.length
 				
-				getProductInfoById(this.editProductId).then(res=>{
-					if (res.data.code === 200) {
-						  // const { data } = res.data;
-						 // this.formData = data
-						 Object.assign(this.formData, res.data.data)  // 保持响应式
-						 // 更新字符计数
-						 this.nameLength = this.formData.title.length;
-						 this.descLength = this.formData.description.length;
-						 
-						  console.log("this.goodsDetail", this.formData);
-						  // 处理图片数据
-						  if (this.formData.imageUrl) {
-						    try {
-							   this.formData.images = this.formData.imageUrl.split(',').map(url => ({ url,status: 'success' }))
-						      console.log('解析后的图片数据:', this.formData.images);
-						    } catch (e) {
-						      console.error('解析图片数据失败:', e);
-						      this.formData.images = [];
-								}
-						  } else {
-						    this.formData.images = [];
-						  }
-						  uni.hideLoading();
-						} else {
-						  uni.showToast({
-						    title: res.data.msg || '获取农品信息失败',
-						    icon: 'none'
-						  });
-						}
+				console.log("this.goodsDetail", formData)
+				// 处理图片数据
+				if (formData.imageUrl) {
+					try {
+						formData.images = formData.imageUrl.split(',').map(url => ({ url, status: 'success' }))
+						console.log('解析后的图片数据:', formData.images)
+					} catch (e) {
+						console.error('解析图片数据失败:', e)
+						formData.images = []
+					}
+				} else {
+					formData.images = []
+				}
+				uni.hideLoading()
+			} else {
+				uni.showToast({
+					title: res.data.msg || '获取农品信息失败',
+					icon: 'none'
 				})
-			},
-
-			// 产品名称输入处理
-			onNameInput(e) {
-				this.nameLength = e.detail.value.length;
-			},
-
-			// 描述输入处理
-			onDescInput(e) {
-				this.descLength = e.detail.value.length;
-			},
-
-			// 选择分类
-			selectCategory(category) {
-				this.formData.categoryId = category.dictValue;
-				this.formData.categoryLabel = category.dictLabel;
-				this.showCategoryPicker = false;
-			},
-			// 选择单位
-			selectUnit(unit) {
-				this.formData.unit = unit.dictValue;
-				this.formData.dictLabel = unit.dictLabel;
-				this.showUnitPicker = false;
-			},
-			
-			getDictLabel(dictKey, value) {
-			   if (!this.dictData || !this.dictData[dictKey]) {
-			     return ''
-			   }
-			   const list = this.dictData[dictKey] || []
-			   const item = list.find(u => u.dictValue == value)
-			   return item ? item.dictLabel : ''
-			 },
-
-			// 选择图片
-			chooseImage() {
-			  uni.chooseImage({
-			    count: 6 - this.formData.images.length,
+			}
+		})
+	}
+
+	// 产品名称输入处理
+	const onNameInput = (e) => {
+		nameLength.value = e.detail.value.length
+	}
+
+	// 描述输入处理
+	const onDescInput = (e) => {
+		descLength.value = e.detail.value.length
+	}
+
+	// 选择分类
+	const selectCategory = (category) => {
+		formData.categoryId = category.dictValue
+		formData.categoryLabel = category.dictLabel
+		showCategoryPicker.value = false
+	}
+
+	// 选择单位
+	const selectUnit = (unit) => {
+		formData.unit = unit.dictValue
+		formData.dictLabel = unit.dictLabel
+		showUnitPicker.value = false
+	}
+	
+	// 获取字典标签
+	const getDictLabel = (dictKey, value) => {
+		if (!dictData.value || !dictData.value[dictKey]) {
+			return ''
+		}
+		const list = dictData.value[dictKey] || []
+		const item = list.find(u => u.dictValue == value)
+		return item ? item.dictLabel : ''
+	}
+
+	// 选择图片
+	const chooseImage = () => {
+		uni.chooseImage({
+			count: 6 - formData.images.length,
 			    sizeType: ['original', 'compressed'],
 			    sourceType: ['album', 'camera'],
 			    success: (res) => {
@@ -402,21 +402,21 @@
 			      
 			      // 如果有有效文件,则上传
 			      if (validFiles.length > 0) {
-			        this.uploadImages(validFiles);
+			        uploadImages(validFiles)
 			      }
 			    },
 			    fail: (err) => {
-			      console.error('选择图片失败:', err);
+			      console.error('选择图片失败:', err)
 			      uni.showToast({
 			        title: '选择图片失败',
 			        icon: 'none'
-			      });
+			      })
 			    }
-			  });
-			},
-			
-			// 上传图片到服务器
-			uploadImages(tempFilePaths) {
+		})
+	}
+	
+	// 上传图片到服务器
+	const uploadImages = (tempFilePaths) => {
 			  uni.showLoading({
 			    title: '上传中...',
 			    mask: true
@@ -489,202 +489,186 @@
 			        if (successCount + failCount === totalFiles) {
 			          if (newImages.length > 0) {
 			            // 将新上传的图片添加到已有图片列表
-			            this.formData.images = [...this.formData.images, ...newImages];
+			            formData.images = [...formData.images, ...newImages]
 			            // 更新taskImages字段,将图片URL用逗号连接
-			           this.formData.imageUrl = this.formData.images.map(item => item.url).join(',');
-					   this.$set(this.formData, 'images', this.formData.images)  // 确保视图刷新
+			            formData.imageUrl = formData.images.map(item => item.url).join(',')
 			            // 显示成功提示
-			            uni.hideLoading();
+			            uni.hideLoading()
 			            uni.showToast({
 			              title: `成功上传${successCount}张图片`,
 			              icon: 'success'
-			            });
+			            })
 			          } else {
 			            // 全部失败
 						uni.showToast({
 						  title: `图片上传: ${newImages}`,
-						  icon: 'none',
-						});
-			            uni.hideLoading();
-			            // uni.showToast({
-			            //   title: '图片上传失败',
-			            //   icon: 'none',
-			            // });
+						  icon: 'none'
+						})
+			            uni.hideLoading()
 			          }
 			        }
 			      }
-			    });
-			  });
-			},
-
-			removeImage(index) {
-			  uni.showModal({
-			    title: '确认删除',
-			    content: '确定要删除这张图片吗?',
-			    success: (res) => {
-			      if (res.confirm) {
-					  console.log("删除钱:",this.formData.images,index);
-			        this.formData.images.splice(index, 1);
-			        
-			        // 更新taskImages字段
-			       this.formData.imageUrl = this.formData.images.map(item => item.url).join(',');
-				   this.$set(this.formData, 'images', this.formData.images)  // 确保视图刷新
-			        console.log("删除后:",this.formData.imageUrl);
-			        uni.showToast({
-			          title: '已删除',
-			          icon: 'none'
-			        });
-			      }
-			    }
-			  });
-			},
-
-			// 表单验证
-			validateForm() {
-				if (!this.formData.title.trim()) {
+			    })
+			  })
+	}
+
+	const removeImage = (index) => {
+		uni.showModal({
+			title: '确认删除',
+			content: '确定要删除这张图片吗?',
+			success: (res) => {
+				if (res.confirm) {
+					console.log("删除前:", formData.images, index)
+					formData.images.splice(index, 1)
+					
+					// 更新taskImages字段
+					formData.imageUrl = formData.images.map(item => item.url).join(',')
+					console.log("删除后:", formData.imageUrl)
 					uni.showToast({
-						title: '请输入产品名称',
+						title: '已删除',
 						icon: 'none'
-					});
-					return false;
+					})
 				}
+			}
+		})
+	}
 
-				if (!this.formData.categoryId) {
-					uni.showToast({
-						title: '请选择产品分类',
-						icon: 'none'
-					});
-					return false;
-				}
+	// 表单验证
+	const validateForm = () => {
+		if (!formData.title.trim()) {
+			uni.showToast({
+				title: '请输入产品名称',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (!this.formData.price || this.formData.price <= 0) {
-					uni.showToast({
-						title: '请输入有效的销售价格',
-						icon: 'none'
-					});
-					return false;
-				}
-				
-				if (!this.formData.quantity || this.formData.quantity <= 0) {
-				  uni.showToast({
-				    title: '请输入有效的收购数量',
-				    icon: 'none'
-				  });
-				  return false;
-				}
+		if (!formData.categoryId) {
+			uni.showToast({
+				title: '请选择产品分类',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (this.formData.imageUrl.length === 0) {
-					uni.showToast({
-						title: '请至少上传一张产品图片',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.price || formData.price <= 0) {
+			uni.showToast({
+				title: '请输入有效的销售价格',
+				icon: 'none'
+			})
+			return false
+		}
+		
+		if (!formData.quantity || formData.quantity <= 0) {
+			uni.showToast({
+				title: '请输入有效的收购数量',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (!this.formData.contactName.trim()) {
-					uni.showToast({
-						title: '请输入联系人姓名',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (formData.imageUrl.length === 0) {
+			uni.showToast({
+				title: '请至少上传一张产品图片',
+				icon: 'none'
+			})
+			return false
+		}
 
-				if (!this.formData.contactPhone.trim()) {
-					uni.showToast({
-						title: '请输入联系电话',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.contactName.trim()) {
+			uni.showToast({
+				title: '请输入联系人姓名',
+				icon: 'none'
+			})
+			return false
+		}
 
-				// 简单的手机号格式验证
-				const phoneRegex = /^1[3-9]\d{9}$/;
-				if (!phoneRegex.test(this.formData.contactPhone)) {
-					uni.showToast({
-						title: '请输入正确的手机号码',
-						icon: 'none'
-					});
-					return false;
-				}
+		if (!formData.contactPhone.trim()) {
+			uni.showToast({
+				title: '请输入联系电话',
+				icon: 'none'
+			})
+			return false
+		}
 
-				return true;
-			},
+		// 简单的手机号格式验证
+		const phoneRegex = /^1[3-9]\d{9}$/
+		if (!phoneRegex.test(formData.contactPhone)) {
+			uni.showToast({
+				title: '请输入正确的手机号码',
+				icon: 'none'
+			})
+			return false
+		}
 
-			// 提交表单
-			submitForm() {
-				if (!this.validateForm()) {
-					return;
-				}
+		return true
+	}
 
-				// 显示加载提示
-				const loadingTitle = this.isEditMode ? '保存中...' : '提交中...';
-				uni.showLoading({
-					title: loadingTitle
-				});
+	// 提交表单
+	const submitForm = () => {
+		if (!validateForm()) {
+			return
+		}
 
-				uni.hideLoading();
-				
-				if (this.isEditMode) {
-					editProductInfo(this.formData).then(res=>{
-						if(res.data.code === 200){
-							uni.showModal({
-								title: '保存成功',
-								content: '您的修改已保存成功!',
-								showCancel: false,
-								success: () => {
-									uni.navigateBack();
-								}
-							});
-						}else{
-							uni.showToast({
-								title: res.data.msg || '提交失败,请稍后重试',
-								icon: 'none'
-							});
+		// 显示加载提示
+		const loadingTitle = isEditMode.value ? '保存中...' : '提交中...'
+		uni.showLoading({
+			title: loadingTitle
+		})
+
+		uni.hideLoading()
+		
+		if (isEditMode.value) {
+			editProductInfo(formData).then(res => {
+				if (res.data.code === 200) {
+					uni.showModal({
+						title: '保存成功',
+						content: '您的修改已保存成功!',
+						showCancel: false,
+						success: () => {
+							uni.navigateBack()
 						}
-					}).catch(err => {
-						console.error("提交异常:", err);
-						uni.showToast({
-							title: '网络错误,请检查后重试',
-							icon: 'none'
-						});
-					});
-					
+					})
 				} else {
-					// 新建模式 - 提交审核
-					console.log("this.formData",this.formData);
-					// const data = {
-					// 	userId: this.currentUserInfo.userid,
-					// 	type: 0 ,// 出售
-					// 	status: 1 ,// 审核中
-					// }
-					// this.formData = {...this.formData , ...data}
-					addProductInfo(this.formData).then(res=>{
-						console.log("新增出售农产品",res);
-						if(res.data.code === 200){
-							uni.showModal({
-								title: '提交成功',
-								content: '您的农产品信息已提交审核,审核通过后将在销售页面展示。',
-								showCancel: false,
-								success: () => {
-									uni.navigateBack();
-								}
-							});
-						}else{
-							uni.showToast({
-								title: res.data.msg || '提交失败,请稍后重试',
-								icon: 'none'
-							});
+					uni.showToast({
+						title: res.data.msg || '提交失败,请稍后重试',
+						icon: 'none'
+					})
+				}
+			}).catch(err => {
+				console.error("提交异常:", err)
+				uni.showToast({
+					title: '网络错误,请检查后重试',
+					icon: 'none'
+				})
+			})
+		} else {
+			// 新建模式 - 提交审核
+			console.log("this.formData", formData)
+			addProductInfo(formData).then(res => {
+				console.log("新增出售农产品", res)
+				if (res.data.code === 200) {
+					uni.showModal({
+						title: '提交成功',
+						content: '您的农产品信息已提交审核,审核通过后将在销售页面展示。',
+						showCancel: false,
+						success: () => {
+							uni.navigateBack()
 						}
-					}).catch(err => {
-						console.error("提交异常:", err);
-						uni.showToast({
-							title: '网络错误,请检查后重试',
-							icon: 'none'
-						});
-					});
-					
+					})
+				} else {
+					uni.showToast({
+						title: res.data.msg || '提交失败,请稍后重试',
+						icon: 'none'
+					})
 				}
-			}
+			}).catch(err => {
+				console.error("提交异常:", err)
+				uni.showToast({
+					title: '网络错误,请检查后重试',
+					icon: 'none'
+				})
+			})
 		}
 	}
 </script>

+ 145 - 161
pages/service/sales.vue

@@ -77,167 +77,151 @@
 	</view>
 </template>
 
-<script>
-	import {
-		getProductInfoList
-	} from '@/api/services/productInfo.js';
-	import dictMixin from '@/utils/mixins/dictMixin';
-	export default {
-		mixins: [dictMixin],
-		data() {
-			return {
-				dictTypeList: ['agricultural_unit'],
-				activeTab: 'all', // 当前活跃的标签页
-				searchKeyword: '',
-				pageNum: 1,
-				pageSize: 10,
-				noMoreData: false,
-				isLoading: false,
-				isRefreshing: false,
-				// 全部农产品信息列表(出售+收购)
-				allInfoList: []
-			}
-		},
-
-		computed: {
-			filteredAllInfo() {
-				let list = this.allInfoList;
-
-				// 按类型筛选
-				if (this.activeTab === 'sale') {
-					list = list.filter(item => item.type === 0);
-				} else if (this.activeTab === 'purchase') {
-					list = list.filter(item => item.type === 1);
-				}
-
-				// 按搜索关键词筛选
-				// if (this.searchKeyword.trim()) {
-				// 	const keyword = this.searchKeyword.trim().toLowerCase();
-				// 	list = list.filter(item =>
-				// 		item.title.toLowerCase().includes(keyword) ||
-				// 		item.description.toLowerCase().includes(keyword) ||
-				// 		item.publisher.toLowerCase().includes(keyword)
-				// 	);
-				// }
-
-				return list;
-			}
-		},
-		// 页面加载时获取数据
-		onLoad() {
-			this.loadProductInfo();
-		},
-		methods: {
-			// 上拉加载更多
-			loadMore() {
-				if (!this.isLoading && !this.noMoreData) {
-					this.pageNum++;
-					this.loadProductInfo();
-				}
-			},
-			getImageUrl(item) {
-				// 默认返回url或path
-				if (item == null) return
-				const imageUrls = item.split(',');
-				// console.log("imageUrls", imageUrls);
-				return imageUrls[0];
-			},
-			getUnitLabel(value) {
-				const unit = this.dictData.agricultural_unit.find(u => u.dictValue == value)
-				return unit ? unit.dictLabel : ''
-			},
-			loadProductInfo(keyword) {
-				this.isLoading = true;
-				uni.showLoading({
-					title: '加载中'
-				});
-				// 构建查询参数
-				const params = {
-					pageNum: this.pageNum,
-					pageSize: this.pageSize,
-					status: 2 // 查询 已上架
-				};
-				if (keyword != null && keyword != '') {
-					params.searchKeyword = keyword
-				}
-
-				// 分类逻辑
-				if (this.activeTab !== 'all' ) {
-					// 默认场景:推荐查询
-					params.type = this.activeTab === 'sale' ? 0 : 1;
-				}
-
-				getProductInfoList(params).then(res => {
-					if (res.data.code === 200) {
-						const {
-							rows,
-							total
-						} = res.data;
-						if (this.pageNum === 1) {
-							this.allInfoList = rows;
-						} else {
-							this.allInfoList = [...this.allInfoList, ...rows];
-						}
-						// 判断是否还有更多数据
-						this.noMoreData = this.allInfoList.length >= total;
-						uni.hideLoading();
-					} else {
-						uni.showToast({
-							title: res.data.msg || '获取农品列表失败',
-							icon: 'none'
-						});
-					}
-				}).catch(err => {
-					console.error('获取农品列表失败:', err);
-					uni.showToast({
-						title: '获取农品列表失败',
-						icon: 'none'
-					});
-				}).finally(() => {
-					this.isLoading = false;
-					this.isRefreshing = false;
-				});
-			},
-			// 切换Tab标签页
-			switchTab(tab) {
-				this.activeTab = tab;
-				this.allInfoList = [] // 切换时置空数据数组
-				// 滚动到当前分类位置
-				this.loadProductInfo(this.searchKeyword.trim())
-			},
-
-			// 获取指示器位置
-			getIndicatorPosition() {
-				const positions = {
-					'all': '0%',
-					'sale': '33.33%',
-					'purchase': '66.66%'
-				};
-				return positions[this.activeTab] || '0%';
-			},
-
-			// 搜索处理
-			handleSearch() {
-				// 搜索逻辑已在computed中处理
-				const keyword = this.searchKeyword.trim().toLowerCase();
-				this.loadProductInfo(keyword)
-			},
-
-			// 导航到详情页
-			navigateToDetail(item) {
-				uni.navigateTo({
-					url: `/pages/service/sales-detail?id=${item.id}&type=${item.type}&title=${encodeURIComponent(item.title)}`
-				});
-			},
-
-			// 导航到我的发布页面
-			navigateToMyPublish() {
-				uni.navigateTo({
-					url: '/pages/service/my-publish'
-				});
-			}
-		}
-	}
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { getProductInfoList } from '@/api/services/productInfo.js'
+import { useDict } from '@/utils/composables/useDict'
+
+// 使用字典
+const { dictData } = useDict(['agricultural_unit'])
+
+const activeTab = ref('all')
+const searchKeyword = ref('')
+const pageNum = ref(1)
+const pageSize = ref(10)
+const noMoreData = ref(false)
+const isLoading = ref(false)
+const isRefreshing = ref(false)
+const allInfoList = ref([])
+
+const filteredAllInfo = computed(() => {
+  let list = allInfoList.value
+  
+  // 按类型筛选
+  if (activeTab.value === 'sale') {
+    list = list.filter(item => item.type === 0)
+  } else if (activeTab.value === 'purchase') {
+    list = list.filter(item => item.type === 1)
+  }
+  
+  return list
+})
+
+// 页面加载时获取数据
+onMounted(() => {
+  loadProductInfo()
+})
+
+// 上拉加载更多
+const loadMore = () => {
+  if (!isLoading.value && !noMoreData.value) {
+    pageNum.value++
+    loadProductInfo()
+  }
+}
+
+const getImageUrl = (item) => {
+  if (item == null) return
+  const imageUrls = item.split(',')
+  return imageUrls[0]
+}
+
+const getUnitLabel = (value) => {
+  const unit = dictData.agricultural_unit?.find(u => u.dictValue == value)
+  return unit ? unit.dictLabel : ''
+}
+
+const loadProductInfo = (keyword) => {
+  isLoading.value = true
+  uni.showLoading({
+    title: '加载中'
+  })
+  
+  // 构建查询参数
+  const params = {
+    pageNum: pageNum.value,
+    pageSize: pageSize.value,
+    status: 2 // 查询 已上架
+  }
+  
+  if (keyword != null && keyword != '') {
+    params.searchKeyword = keyword
+  }
+  
+  // 分类逻辑
+  if (activeTab.value !== 'all') {
+    params.type = activeTab.value === 'sale' ? 0 : 1
+  }
+  
+  getProductInfoList(params).then(res => {
+    if (res.data.code === 200) {
+      const { rows, total } = res.data
+      if (pageNum.value === 1) {
+        allInfoList.value = rows
+      } else {
+        allInfoList.value = [...allInfoList.value, ...rows]
+      }
+      // 判断是否还有更多数据
+      noMoreData.value = allInfoList.value.length >= total
+      uni.hideLoading()
+    } else {
+      uni.showToast({
+        title: res.data.msg || '获取农品列表失败',
+        icon: 'none'
+      })
+    }
+  }).catch(err => {
+    console.error('获取农品列表失败:', err)
+    uni.showToast({
+      title: '获取农品列表失败',
+      icon: 'none'
+    })
+  }).finally(() => {
+    isLoading.value = false
+    isRefreshing.value = false
+  })
+}
+
+// 切换Tab标签页
+const switchTab = (tab) => {
+  activeTab.value = tab
+  allInfoList.value = [] // 切换时置空数据数组
+  pageNum.value = 1 // 重置页码
+  // 滚动到当前分类位置
+  loadProductInfo(searchKeyword.value.trim())
+}
+
+// 获取指示器位置
+const getIndicatorPosition = () => {
+  const positions = {
+    'all': '0%',
+    'sale': '33.33%',
+    'purchase': '66.66%'
+  }
+  return positions[activeTab.value] || '0%'
+}
+
+// 搜索处理
+const handleSearch = () => {
+  const keyword = searchKeyword.value.trim().toLowerCase()
+  pageNum.value = 1 // 重置页码
+  allInfoList.value = [] // 清空列表
+  loadProductInfo(keyword)
+}
+
+// 导航到详情页
+const navigateToDetail = (item) => {
+  uni.navigateTo({
+    url: `/pages/service/sales-detail?id=${item.id}&type=${item.type}&title=${encodeURIComponent(item.title)}`
+  })
+}
+
+// 导航到我的发布页面
+const navigateToMyPublish = () => {
+  uni.navigateTo({
+    url: '/pages/service/my-publish'
+  })
+}
 </script>
 
 <style lang="scss">

+ 104 - 110
pages/settings/index.vue

@@ -57,116 +57,110 @@
   </view>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      // 通知设置状态
-      notifications: {
-        system: true,
-        crop: true,
-        weather: true
-      },
-      
-      // 缓存大小
-      cacheSize: '0.00MB',
-      
-      // 版本号
-      version: '1.0.0'
-    };
-  },
-  
-  methods: {
-    // 获取缓存大小
-    getCacheSize() {
-      // 实际项目中需要调用相关API获取缓存大小
-      uni.getStorageInfo({
-        success: (res) => {
-          const size = (res.currentSize / 1024).toFixed(2);
-          this.cacheSize = size + 'MB';
-        }
-      });
-    },
-    
-    // 清除缓存
-    handleClearCache() {
-      uni.showModal({
-        title: '提示',
-        content: '确定要清除缓存吗?',
-        success: (res) => {
-          if (res.confirm) {
-            uni.clearStorage({
-              success: () => {
-                uni.showToast({
-                  title: '清除成功',
-                  icon: 'success'
-                });
-                this.getCacheSize();
-              }
-            });
-          }
-        }
-      });
-    },
-    
-    // 检查更新
-    checkUpdate() {
-      // 实际项目中需要调用后端API检查更新
-      uni.showLoading({ title: '检查更新中...' });
-      setTimeout(() => {
-        uni.hideLoading();
-        uni.showToast({
-          title: '已是最新版本',
-          icon: 'none'
-        });
-      }, 1500);
-    },
-    
-    // 通知设置处理函数
-    handleSystemNotification(e) {
-      this.notifications.system = e.detail.value;
-      this.saveNotificationSettings();
-    },
-    
-    handleCropNotification(e) {
-      this.notifications.crop = e.detail.value;
-      this.saveNotificationSettings();
-    },
-    
-    handleWeatherNotification(e) {
-      this.notifications.weather = e.detail.value;
-      this.saveNotificationSettings();
-    },
-    
-    // 保存通知设置
-    saveNotificationSettings() {
-      uni.setStorageSync('notifications', this.notifications);
-    },
-    
-    // 加载通知设置
-    loadNotificationSettings() {
-      const savedSettings = uni.getStorageSync('notifications');
-      if (savedSettings) {
-        this.notifications = savedSettings;
-      }
-    },
-    
-    // 页面跳转
-    navigateToAboutUs() {
-      uni.navigateTo({ url: '/pages/about/index' });
-    },
-    
-    navigateToPrivacy() {
-      uni.navigateTo({ url: '/pages/privacy/index' });
-    }
-  },
-  
-  // 页面显示时加载数据
-  onShow() {
-    this.getCacheSize();
-    this.loadNotificationSettings();
-  }
-};
+<script setup>
+import { ref, reactive } from 'vue'
+
+// 响应式数据
+const notifications = reactive({
+	system: true,
+	crop: true,
+	weather: true
+})
+
+const cacheSize = ref('0.00MB')
+const version = ref('1.0.0')
+
+// 获取缓存大小
+const getCacheSize = () => {
+	uni.getStorageInfo({
+		success: (res) => {
+			const size = (res.currentSize / 1024).toFixed(2)
+			cacheSize.value = size + 'MB'
+		}
+	})
+}
+
+// 清除缓存
+const handleClearCache = () => {
+	uni.showModal({
+		title: '提示',
+		content: '确定要清除缓存吗?',
+		success: (res) => {
+			if (res.confirm) {
+				uni.clearStorage({
+					success: () => {
+						uni.showToast({
+							title: '清除成功',
+							icon: 'success'
+						})
+						getCacheSize()
+					}
+				})
+			}
+		}
+	})
+}
+
+// 检查更新
+const checkUpdate = () => {
+	uni.showLoading({ title: '检查更新中...' })
+	setTimeout(() => {
+		uni.hideLoading()
+		uni.showToast({
+			title: '已是最新版本',
+			icon: 'none'
+		})
+	}, 1500)
+}
+
+// 通知设置处理函数
+const handleSystemNotification = (e) => {
+	notifications.system = e.detail.value
+	saveNotificationSettings()
+}
+
+const handleCropNotification = (e) => {
+	notifications.crop = e.detail.value
+	saveNotificationSettings()
+}
+
+const handleWeatherNotification = (e) => {
+	notifications.weather = e.detail.value
+	saveNotificationSettings()
+}
+
+// 保存通知设置
+const saveNotificationSettings = () => {
+	uni.setStorageSync('notifications', notifications)
+}
+
+// 加载通知设置
+const loadNotificationSettings = () => {
+	const savedSettings = uni.getStorageSync('notifications')
+	if (savedSettings) {
+		Object.assign(notifications, savedSettings)
+	}
+}
+
+// 页面跳转
+const navigateToAboutUs = () => {
+	uni.navigateTo({ url: '/pages/about/index' })
+}
+
+const navigateToPrivacy = () => {
+	uni.navigateTo({ url: '/pages/privacy/index' })
+}
+
+// uni-app 页面生命周期
+const onShow = () => {
+	getCacheSize()
+	loadNotificationSettings()
+}
+
+// 导出页面生命周期方法供 uni-app 调用
+defineExpose({
+	onShow
+})
 </script>
 
 <style lang="scss">

+ 162 - 165
pages/user/index.vue

@@ -85,176 +85,173 @@
 	</view>
 </template>
 
-<script>
-	import {
-		logout, webLogout
-	} from '@/api/services/connect.js';
-	import {
-		countUserPlots
-	} from '@/api/services/field.js';
-	import storage from "@/utils/storage.js";
-	export default {
-		data() {
-			return {
-				plotInfo: {
-					total: 0,
-					active: 0,
-					idle: 0
-				},
-				serviceList: [
-        { 
-          name: '农资商城',
-          iconText: '商',
-          iconSvg: '/static/icons/mall.png',
-          path: '/pages/service/mall'
-        },
-        { 
-          name: '农品交易',
-          iconText: '售',
-          iconSvg: '/static/icons/sales.png',
-          path: '/pages/service/sales'
-        },
-        { 
-          name: '专家咨询',
-          iconText: '诊',
-          iconSvg: '/static/icons/expert.png',
-          path: '/pages/service/expert'
-        },
-        { 
-          name: '绿色认证',
-          iconText: '证',
-          iconSvg: '/static/icons/certification.png',
-          path: '/pages/service/certification'
-        },
-        { 
-          name: '保险接入',
-          iconText: '保',
-          iconSvg: '/static/icons/insurance.png',
-          path: '/pages/service/insurance'
-        }
-      ],
-				userInfo: {
-					nickName: '',
-					id: '',
-					avatar: '',
-					sex:''
-				},
-				isLogin: false
-			}
-		},
-		onShow() {
-			this.checkLoginStatus();
-			if(this.isLogin){
-				this.userPlots();
-			}
-		},
-		methods: {
-			userPlots(){
-				countUserPlots().then((res=>{
-					if (res.data.code == 200) {
-						const {plotsTotal,inUseCount,leiSureCount} = res.data.data
-						this.plotInfo = {
-							total: plotsTotal,
-							active: inUseCount,
-							idle: leiSureCount
-						}
-					} else{
-						console.error("统计地块数量失败!")
-					}
-				}))
-			},
-			// 检查登录状态
-			checkLoginStatus() {
-				console.log("执行Show");
-				if (storage.getHasLogin()) {
-					this.isLogin = true;
-					// 获取用户信息
-					const userInfo = storage.getUserInfo();
-					console.log("执行Show",userInfo);
-					if (userInfo) {
-						this.userInfo = {
-							nickName: userInfo.username || '游客',
-							avatar: userInfo.avatar || '/static/icons/user_icon.png',
-							sex:userInfo.sex
-						};
-					}
-				} else {
-					this.isLogin = false;
-				}
-			},
-			navigateToLogin() {
-				uni.navigateTo({
-					url: '/pages/login/index'
-				});
-			},
-			navigateToPlots() {
-				uni.navigateTo({
-					url: '/pages/plots/list'
-				})
-			},
-    navigateToService(item) {
-      uni.navigateTo({ url: item.path })
-    },
-			handleContact() {
-				uni.makePhoneCall({
-					phoneNumber: '400-888-8888' // 替换为实际的客服电话
-				})
-			},
-			navigateToAbout() {
-				uni.navigateTo({
-					url: '/pages/about/index'
-				})
-			},
-			navigateToSettings() {
-				uni.navigateTo({
-					url: '/pages/settings/index'
+<script setup>
+import { ref, reactive } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import { logout, webLogout } from '@/api/services/connect.js'
+import { countUserPlots } from '@/api/services/field.js'
+import storage from "@/utils/storage.js"
+
+// 响应式数据
+const plotInfo = reactive({
+	total: 0,
+	active: 0,
+	idle: 0
+})
+
+const serviceList = ref([
+	{ 
+		name: '农资商城',
+		iconText: '商',
+		iconSvg: '/static/icons/mall.png',
+		path: '/pages/service/mall'
+	},
+	{ 
+		name: '农品交易',
+		iconText: '售',
+		iconSvg: '/static/icons/sales.png',
+		path: '/pages/service/sales'
+	},
+	{ 
+		name: '专家咨询',
+		iconText: '诊',
+		iconSvg: '/static/icons/expert.png',
+		path: '/pages/service/expert'
+	},
+	{ 
+		name: '绿色认证',
+		iconText: '证',
+		iconSvg: '/static/icons/certification.png',
+		path: '/pages/service/certification'
+	},
+	{ 
+		name: '保险接入',
+		iconText: '保',
+		iconSvg: '/static/icons/insurance.png',
+		path: '/pages/service/insurance'
+	}
+])
+
+const userInfo = reactive({
+	nickName: '',
+	id: '',
+	avatar: '',
+	sex: ''
+})
+
+const isLogin = ref(false)
+
+// 方法
+const userPlots = () => {
+	countUserPlots().then((res => {
+		if (res.data.code == 200) {
+			const { plotsTotal, inUseCount, leiSureCount } = res.data.data
+			plotInfo.total = plotsTotal
+			plotInfo.active = inUseCount
+			plotInfo.idle = leiSureCount
+		} else {
+			console.error("统计地块数量失败!")
+		}
+	}))
+}
+
+// 检查登录状态
+const checkLoginStatus = () => {
+	console.log("执行Show")
+	if (storage.getHasLogin()) {
+		isLogin.value = true
+		// 获取用户信息
+		const storedUserInfo = storage.getUserInfo()
+		console.log("执行Show", storedUserInfo)
+		if (storedUserInfo) {
+			userInfo.nickName = storedUserInfo.username || '游客'
+			userInfo.avatar = storedUserInfo.avatar || '/static/icons/user_icon.png'
+			userInfo.sex = storedUserInfo.sex
+		}
+	} else {
+		isLogin.value = false
+	}
+}
+
+const navigateToLogin = () => {
+	uni.navigateTo({
+		url: '/pages/login/index'
+	})
+}
+
+const navigateToPlots = () => {
+	uni.navigateTo({
+		url: '/pages/plots/list'
+	})
+}
+
+const navigateToService = (item) => {
+	uni.navigateTo({ url: item.path })
+}
+
+const handleContact = () => {
+	uni.makePhoneCall({
+		phoneNumber: '400-888-8888' // 替换为实际的客服电话
+	})
+}
+
+const navigateToAbout = () => {
+	uni.navigateTo({
+		url: '/pages/about/index'
+	})
+}
+
+const navigateToSettings = () => {
+	uni.navigateTo({
+		url: '/pages/settings/index'
+	})
+}
+
+// 使用授权服务处理登出
+const handleLogout = () => {
+	uni.showModal({
+		title: '提示',
+		content: '确认退出登录?',
+		success: (res) => {
+			if (res.confirm) {
+				// #ifdef H5
+				webLogout().then(res => {
+					console.log("tuichu", res)
+					storage.setAccessToken("")
+					storage.setUserInfo({})
+					storage.setHasLogin(false)
+					storage.removeCurrentPlot()
+					storage.removeUserPlots()
+					isLogin.value = false
+					userInfo.nickname = '游客'
+					userInfo.id = ''
+					userInfo.avatar = ''
+					plotInfo.total = 0
+					plotInfo.active = 0
+					plotInfo.idle = 0
 				})
-			},
-			// 使用授权服务处理登出
-			handleLogout() {
-				uni.showModal({
-					title: '提示',
-					content: '确认退出登录?',
-					success: (res) => {
-						if (res.confirm) {
-							// #ifdef H5
-							webLogout().then(res=>{
-								console.log("tuichu",res);
-								storage.setAccessToken("");
-								storage.setUserInfo({});
-								storage.setHasLogin(false)
-								storage.removeCurrentPlot()
-								storage.removeUserPlots()
-								this.isLogin = false;
-								this.userInfo = {
-									nickname: '游客',
-									id: '',
-									avatar: ''
-								};
-								this.plotInfo = {
-									total: 0,
-									active: 0,
-									idle: 0
-								}
-							})
-							// #endif
-							
-							// #ifdef APP-PLUS
-							logout().then(() => {
-								this.isLogin = false;
-								this.userInfo = {
-									nickname: '游客',
-									id: '',
-									avatar: '/static/icons/user_icon'
-								};
-							});
-							// #endif
-						}
-					}
+				// #endif
+				
+				// #ifdef APP-PLUS || MP-HARMONY
+				logout().then(() => {
+					isLogin.value = false
+					userInfo.nickname = '游客'
+					userInfo.id = ''
+					userInfo.avatar = '/static/icons/user_icon'
 				})
+				// #endif
 			}
 		}
+	})
+}
+
+// 生命周期钩子
+onShow(() => {
+	checkLoginStatus()
+	if (isLogin.value) {
+		userPlots()
 	}
+})
 </script>
 
 <style>

+ 85 - 83
pages/userInfo/index.vue

@@ -39,90 +39,92 @@
 	</view>
 </template>
 
-<script>
-	import {
-		uploadInfo
-	} from "@/api/services/connect.js";
-	import storage from "@/utils/storage.js";
-	export default {
-		data() {
-			return {
-				isShow: true,
-				userInfo: {
-					avatarUrl: '',
-					nickName: '',
-					openId: ''
-				},
-				loading: false,
-			};
-		},
-		methods: {
-			//获取微信头像
-			onChooseAvatar(e) {
-				this.userInfo.avatarUrl = e.detail.avatarUrl;
-				console.log("eeee", e);
-			},
-			goBack() {
-				uni.navigateBack();
-			},
-
-			async requestProfile() {
-				try {
-					const res = await uni.getUserProfile({
-						desc: '用于完善会员资料',
-					});
-					this.userInfo = res.userInfo;
-				} catch (err) {
-					uni.showToast({
-						icon: 'none',
-						title: '授权失败',
-					});
-				}
-			},
-
-			async loginWithWeChat() {
-				// 防止进入后重复点击(双保险)
-				if (this.loading) return;
-				if (!this.userInfo.nickName) {
-					uni.showToast({
-						title: '请先授权头像昵称',
-						icon: 'none'
-					});
-					return;
-				}
-
-				this.loading = true;
-				try {
-					// TODO: 调用后端接口完成注册/登录
-					const res = await uploadInfo(this.userInfo);
-					console.log("res", res);
-					if (res.data.code == 200) {
-						storage.setUserInfo(res.data.data);
-					}
-					uni.showToast({
-						title: '登录成功',
-					});
-
-					// 成功跳转页面
-					wx.switchTab({
-						url: '/pages/user/index'
-					});
-				} catch (err) {
-					console.log("err", err);
-					uni.showToast({
-						icon: 'none',
-						title: '登录失败,请重试',
-					});
-				} finally {
-					this.loading = false;
-				}
-			},
-		},
-		onLoad(options) {
-			console.log("options", options);
-			this.userInfo.openId = options.openId || '';
+<script setup>
+import { ref, reactive } from 'vue'
+import { uploadInfo } from "@/api/services/connect.js"
+import storage from "@/utils/storage.js"
+
+// 响应式数据
+const isShow = ref(true)
+const userInfo = reactive({
+	avatarUrl: '',
+	nickName: '',
+	openId: ''
+})
+const loading = ref(false)
+
+// 获取微信头像
+const onChooseAvatar = (e) => {
+	userInfo.avatarUrl = e.detail.avatarUrl
+	console.log("eeee", e)
+}
+
+// 返回上一页
+const goBack = () => {
+	uni.navigateBack()
+}
+
+// 请求用户资料
+const requestProfile = async () => {
+	try {
+		const res = await uni.getUserProfile({
+			desc: '用于完善会员资料',
+		})
+		Object.assign(userInfo, res.userInfo)
+	} catch (err) {
+		uni.showToast({
+			icon: 'none',
+			title: '授权失败',
+		})
+	}
+}
+
+// 微信登录
+const loginWithWeChat = async () => {
+	// 防止进入后重复点击(双保险)
+	if (loading.value) return
+	if (!userInfo.nickName) {
+		uni.showToast({
+			title: '请先授权头像昵称',
+			icon: 'none'
+		})
+		return
+	}
+
+	loading.value = true
+	try {
+		// TODO: 调用后端接口完成注册/登录
+		// 原因: 当前使用的是 uploadInfo 接口,需要确认是否为正确的注册/登录接口
+		// 推荐: 确认后端接口文档,使用正确的注册或登录接口
+		const res = await uploadInfo(userInfo)
+		console.log("res", res)
+		if (res.data.code == 200) {
+			storage.setUserInfo(res.data.data)
 		}
-	};
+		uni.showToast({
+			title: '登录成功',
+		})
+
+		// 成功跳转页面
+		uni.switchTab({
+			url: '/pages/user/index'
+		})
+	} catch (err) {
+		console.log("err", err)
+		uni.showToast({
+			icon: 'none',
+			title: '登录失败,请重试',
+		})
+	} finally {
+		loading.value = false
+	}
+}
+
+// uni-app 生命周期 - onLoad
+onLoad((options) => {
+	console.log("options", options)
+	userInfo.openId = options.openId || ''
+})
 </script>
 
 <style>

+ 19 - 10
store/index.js

@@ -1,25 +1,31 @@
-// store/index.js (Vue 2)
-import Vue from 'vue'
-import Vuex from 'vuex'
+// store/index.js (Vue 3)
+import { createStore } from 'vuex'
 import storage from '@/utils/storage'
 
-Vue.use(Vuex)
-
-const store = new Vuex.Store({
+const store = createStore({
   state: {
     isShowToast: false, // 是否在展示Toast中
     remark: [], // 填写订单备注
     shareLink: "", // 分享链接
     verificationKey: "", // 获取key表示验证通过
     distributionId: "", // 分销员Id
-    hasLogin: storage.getHasLogin(),
-    userInfo: storage.getUserInfo(),
-    uuid: storage.getUuid(),
+    hasLogin: false,
+    userInfo: {},
+    uuid: "",
     token: "",
     userName: "",
   },
 
   mutations: {
+    // 初始化状态(从存储中恢复)
+    initState(state) {
+      state.hasLogin = storage.getHasLogin() || false;
+      state.userInfo = storage.getUserInfo() || {};
+      state.uuid = storage.getUuid() || "";
+      if (state.hasLogin && state.userInfo) {
+        state.userName = state.userInfo.Name || state.userInfo.Nickname || state.userInfo.Username || "匿名用户";
+      }
+    },
     login(state, userInfo) {
       state.userInfo = userInfo || {};
       state.userName =
@@ -38,7 +44,10 @@ const store = new Vuex.Store({
   },
 
   actions: {
-    // 可在此添加异步登录逻辑
+    // 初始化 store(在应用启动后调用)
+    init({ commit }) {
+      commit('initState');
+    },
   },
 })
 

+ 11 - 0
types/uview-plus.d.ts

@@ -0,0 +1,11 @@
+declare module 'uview-plus' {
+  import { App } from 'vue'
+  
+  export function setConfig(config: any): void
+  
+  const uviewPlus: {
+    install: (app: App) => void
+  }
+  
+  export default uviewPlus
+}

+ 1 - 2
uni.scss

@@ -1,3 +1,2 @@
 /* 页面左右间距 */
-@import "uview-ui/theme.scss";
-
+@import "uview-plus/theme.scss";

+ 420 - 0
utils/composables/useDict.js

@@ -0,0 +1,420 @@
+/**
+ * 字典数据加载 Composable (Vue 3 Composition API)
+ * 
+ * 使用方式:
+ * 1. 在组件中导入 import { useDict } from '@/utils/composables/useDict';
+ * 2. 在 setup 中调用 const { dictData, dictLoading, loadDict, getDictLabel, ... } = useDict(['sys_user_sex', 'sys_normal_disable']);
+ * 3. 在模板中直接使用 dictData 对象获取字典项 v-for="item in dictData.sys_user_sex"
+ */
+
+import { ref, reactive, onMounted } from 'vue';
+import { getDictData } from '@/api/services/dict';
+import storage from '@/utils/storage';
+import staticDict from '@/utils/staticDict';
+
+// 全局字典缓存对象,用于存储已加载的字典数据
+const dictCache = {
+  // 缓存的字典数据,格式为 { dictType: [{label, value, ...}, ...] }
+  data: {},
+  // 缓存过期时间,单位为毫秒
+  expireTime: 1000 * 60 * 60, // 1小时
+  // 缓存最后更新时间
+  lastUpdateTime: {},
+  // 正在加载的字典类型,用于防止重复请求
+  loading: {}
+};
+
+/**
+ * 字典数据加载 Composable
+ * @param {Array} initialDictTypes - 初始需要加载的字典类型数组
+ * @param {Object} options - 配置选项
+ * @param {Boolean} options.autoLoad - 是否自动加载,默认为 true
+ * @returns {Object} - 返回字典相关的响应式数据和方法
+ */
+export function useDict(initialDictTypes = [], options = {}) {
+  const { autoLoad = true } = options;
+  
+  // 组件中的字典数据
+  const dictData = reactive({});
+  
+  // 字典加载状态
+  const dictLoading = ref(false);
+  
+  // 定义组件需要加载的字典类型
+  const dictTypeList = ref(initialDictTypes);
+
+  /**
+   * 从缓存中获取字典数据
+   * @param {String} dictType - 字典类型
+   * @returns {Array|null} - 返回字典数据,不存在或已过期则返回null
+   */
+  const getDictFromCache = (dictType) => {
+    // 判断是否有缓存
+    if (!dictCache.data[dictType]) {
+      return null;
+    }
+
+    // 判断缓存是否过期
+    const lastUpdateTime = dictCache.lastUpdateTime[dictType] || 0;
+    const now = Date.now();
+    if (now - lastUpdateTime > dictCache.expireTime) {
+      // 缓存已过期,删除缓存
+      delete dictCache.data[dictType];
+      delete dictCache.lastUpdateTime[dictType];
+      return null;
+    }
+
+    // 返回缓存的字典数据
+    return dictCache.data[dictType];
+  };
+
+  /**
+   * 更新字典缓存
+   * @param {String} dictType - 字典类型
+   * @param {Array} dictList - 字典数据列表
+   */
+  const updateDictCache = (dictType, dictList) => {
+    dictCache.data[dictType] = dictList;
+    dictCache.lastUpdateTime[dictType] = Date.now();
+
+    // 更新本地存储
+    try {
+      // 只存储最后更新时间,具体数据保存在内存中
+      storage.setDict(`dict_time_${dictType}`, dictCache.lastUpdateTime[dictType]);
+    } catch (e) {
+      console.error('更新字典缓存失败:', e);
+    }
+  };
+
+  /**
+   * 等待指定类型的字典加载完成
+   * @param {Array} types - 字典类型数组
+   * @returns {Promise} - 返回等待的Promise对象
+   */
+  const waitForDictLoading = (types) => {
+    return new Promise(resolve => {
+      const checkInterval = setInterval(() => {
+        const stillLoading = types.some(type => dictCache.loading[type]);
+        if (!stillLoading) {
+          clearInterval(checkInterval);
+
+          // 加载完成后,从缓存中获取数据
+          types.forEach(type => {
+            const cachedDict = getDictFromCache(type);
+            if (cachedDict) {
+              dictData[type] = cachedDict;
+            }
+          });
+
+          resolve(dictData);
+        }
+      }, 50);
+    });
+  };
+
+  /**
+   * 从服务器获取字典数据
+   * @param {Array} dictTypes - 字典类型数组
+   * @returns {Promise} - 返回字典获取的Promise对象
+   */
+  const fetchDictData = (dictTypes) => {
+    // 标记这些字典类型正在加载
+    dictTypes.forEach(type => {
+      dictCache.loading[type] = true;
+    });
+
+    // 单个字典类型
+    if (dictTypes.length === 1) {
+      const dictType = dictTypes[0];
+      console.log(`[useDict] Fetching single dictionary: ${dictType}`);
+      return getDictData(dictType).then(res => {
+        if (res.data.code === 200) {
+          let dictList = [];
+          if (dictType === 'mall_product_category') {
+            dictList.push({
+              dictLabel: '推荐',
+              dictValue: '-1'
+            });
+            dictList.push(...res.data.data);
+          } else {
+            dictList = res.data.data;
+          }
+
+          // 更新组件数据和缓存
+          dictData[dictType] = dictList;
+          updateDictCache(dictType, dictList);
+          delete dictCache.loading[dictType];
+
+          return dictList;
+        } else {
+          console.error(`获取字典[${dictType}]数据失败:`, res.data.msg);
+          delete dictCache.loading[dictType];
+          return Promise.reject(res.data.msg);
+        }
+      }).catch(err => {
+        delete dictCache.loading[dictType];
+        throw err;
+      });
+    }
+
+    // 多个字典类型,并发请求
+    console.log(`[useDict] Concurrently fetching ${dictTypes.length} dictionaries: ${dictTypes.join(', ')}`);
+
+    const requests = dictTypes.map(dictType => {
+      return getDictData(dictType).then(res => {
+        if (res.data.code === 200) {
+          let dictList = [];
+          if (dictType === 'mall_product_category') {
+            dictList.push({
+              dictLabel: '推荐',
+              dictValue: '-1'
+            });
+            dictList.push(...res.data.data);
+          } else {
+            dictList = res.data.data;
+          }
+
+          dictData[dictType] = dictList;
+          updateDictCache(dictType, dictList);
+          delete dictCache.loading[dictType];
+
+          return {
+            dictType,
+            dictList,
+            success: true
+          };
+        } else {
+          console.error(`获取字典[${dictType}]数据失败:`, res.data.msg);
+          delete dictCache.loading[dictType];
+          return {
+            dictType,
+            dictList: [],
+            success: false,
+            msg: res.data.msg
+          };
+        }
+      }).catch(err => {
+        console.error(`获取字典[${dictType}]异常:`, err);
+        delete dictCache.loading[dictType];
+        return {
+          dictType,
+          dictList: [],
+          success: false,
+          msg: err
+        };
+      });
+    });
+
+    // 等所有请求完成
+    return Promise.allSettled(requests).then(results => {
+      const dictMap = {};
+      const failed = [];
+
+      results.forEach(r => {
+        if (r.status === 'fulfilled') {
+          const { dictType, dictList, success, msg } = r.value;
+          if (success) {
+            dictMap[dictType] = dictList;
+          } else {
+            failed.push({ dictType, msg });
+          }
+        } else {
+          console.error(`字典请求失败:`, r.reason);
+        }
+      });
+
+      console.log(`[useDict] Loaded ${Object.keys(dictMap).length} dictionaries, failed ${failed.length}`);
+
+      if (failed.length > 0) {
+        console.warn('以下字典加载失败:', failed);
+      }
+
+      return dictMap;
+    });
+  };
+
+  /**
+   * 加载字典数据
+   * @param {Array} dictTypes - 字典类型数组,如果不传则使用初始化时的dictTypeList
+   * @returns {Promise} - 返回字典加载的Promise对象
+   */
+  const loadDict = (dictTypes) => {
+    const types = dictTypes || dictTypeList.value;
+    if (!types || types.length === 0) {
+      return Promise.resolve({});
+    }
+
+    // 标记加载中
+    dictLoading.value = true;
+
+    // 需要从服务器获取的字典类型
+    const needFetch = [];
+
+    // 检查是否有静态字典或缓存
+    types.forEach(type => {
+      // 先检查是否有静态字典
+      if (staticDict[type]) {
+        // 使用静态字典数据
+        console.log(`[useDict] Using static dictionary for ${type}`);
+        dictData[type] = staticDict[type];
+      } else {
+        // 检查缓存
+        const cachedDict = getDictFromCache(type);
+        if (cachedDict) {
+          // 已有缓存,直接使用
+          console.log(`[useDict] Using cached dictionary for ${type}`);
+          dictData[type] = cachedDict;
+        } else if (!dictCache.loading[type]) {
+          // 需要从服务器获取,并且当前没有其他组件正在加载
+          console.log(`[useDict] Need to fetch dictionary ${type} from server`);
+          needFetch.push(type);
+        } else {
+          console.log(`[useDict] Dictionary ${type} is already being loaded by another component, waiting...`);
+        }
+      }
+    });
+
+    // 如果所有字典都已缓存或使用静态数据,直接返回
+    if (needFetch.length === 0) {
+      dictLoading.value = false;
+
+      // 检查是否有正在加载的字典,如果有,等待它们完成
+      const loadingTypes = types.filter(type => dictCache.loading[type]);
+      if (loadingTypes.length > 0) {
+        return waitForDictLoading(loadingTypes);
+      }
+
+      return Promise.resolve(dictData);
+    }
+
+    // 从服务器获取字典数据
+    return fetchDictData(needFetch).then(res => {
+      dictLoading.value = false;
+      console.log("dictData", dictData);
+      return dictData;
+    }).catch(err => {
+      dictLoading.value = false;
+      console.error('加载字典数据失败:', err);
+      return Promise.reject(err);
+    });
+  };
+
+  /**
+   * 清除字典缓存
+   * @param {String} dictType - 字典类型,不传则清除所有缓存
+   */
+  const clearDictCache = (dictType) => {
+    if (dictType) {
+      delete dictCache.data[dictType];
+      delete dictCache.lastUpdateTime[dictType];
+      storage.removeDict(`dict_time_${dictType}`);
+    } else {
+      dictCache.data = {};
+      dictCache.lastUpdateTime = {};
+      // 清除所有字典相关的本地存储
+      // TODO: 需要根据平台使用不同的方式获取所有存储的key
+      // 原因: uni-app 没有直接获取所有key的API,需要手动管理字典key列表
+      // 推荐: 维护一个字典key列表,在添加字典时记录key,清除时遍历列表删除
+    }
+  };
+
+  /**
+   * 根据字典值获取对应的字典标签
+   * @param {String} dictType - 字典类型
+   * @param {String|Number} value - 字典值
+   * @param {String} defaultLabel - 默认标签
+   * @returns {String} - 字典标签
+   */
+  const getDictLabel = (dictType, value, defaultLabel = '') => {
+    // 首先检查组件数据
+    const dictList = dictData[dictType];
+    if (dictList) {
+      const item = dictList.find(dict => dict.dictValue === value);
+      if (item) return item.dictLabel;
+    }
+
+    // 都没找到,返回默认值
+    return defaultLabel;
+  };
+
+  /**
+   * 根据字典标签获取对应的字典值
+   * @param {String} dictType - 字典类型
+   * @param {String} label - 字典标签
+   * @param {String|Number} defaultValue - 默认值
+   * @returns {String|Number} - 字典值
+   */
+  const getDictValue = (dictType, label, defaultValue = '') => {
+    // 首先检查组件数据
+    const dictList = dictData[dictType];
+    if (dictList) {
+      const item = dictList.find(dict => dict.label === label);
+      if (item) return item.value;
+    }
+
+    // 再检查静态字典
+    const staticDictList = staticDict[dictType];
+    if (staticDictList) {
+      const item = staticDictList.find(dict => dict.label === label);
+      if (item) return item.value;
+    }
+
+    // 都没找到,返回默认值
+    return defaultValue;
+  };
+
+  /**
+   * 获取字典列表
+   * @param {String} dictType - 字典类型
+   * @returns {Array} - 字典列表
+   */
+  const getDictList = (dictType) => {
+    // 首先检查组件数据
+    const dictList = dictData[dictType];
+    if (dictList) return dictList;
+
+    // 再检查静态字典
+    return staticDict[dictType] || [];
+  };
+
+  /**
+   * 获取字典类型对应的样式类
+   * @param {String} dictType - 字典类型
+   * @param {String|Number} value - 字典值
+   * @param {String} defaultClass - 默认样式类
+   * @returns {String} - 字典项的样式类
+   */
+  const getDictClass = (dictType, value, defaultClass = '') => {
+    // 首先检查组件数据
+    const dictList = dictData[dictType];
+    if (dictList) {
+      const item = dictList.find(dict => dict.dictValue === value);
+      if (item && item.listClass) return item.listClass;
+    }
+
+    // 都没找到,返回默认值
+    return defaultClass;
+  };
+
+  // 自动加载字典
+  if (autoLoad && initialDictTypes.length > 0) {
+    onMounted(() => {
+      console.log(`[useDict] Auto-loading dictionaries: ${initialDictTypes.join(', ')}`);
+      loadDict();
+    });
+  }
+
+  return {
+    dictData,
+    dictLoading,
+    dictTypeList,
+    loadDict,
+    clearDictCache,
+    getDictLabel,
+    getDictValue,
+    getDictList,
+    getDictClass
+  };
+}
+
+export default useDict;

+ 44 - 19
utils/filters.js

@@ -1,11 +1,14 @@
-// import { logout, logoffConfirm } from "@/api/services/login.js";
-// import { getUserInfo } from "@/api/services/members.js";
+/**
+ * 通用过滤器和工具函数
+ * Vue 3 版本 - 过滤器已转换为普通函数
+ * 
+ * 注意: Vue3 移除了过滤器功能,这些函数应该作为方法或全局方法使用
+ * 在 main.js 中通过 app.config.globalProperties 注册为全局方法
+ */
+
 import storage from "@/utils/storage.js";
-// import Vue from "vue";
-// import { getCurrentInstance } from 'vue'
-// 获取当前组件实例
-// const { proxy } = getCurrentInstance()
 import Foundation from "./Foundation.js";
+
 /**
  * 金钱单位置换  2999 --> 2,999.00
  * @param val
@@ -344,7 +347,10 @@ export function isLogin (val) {
 
 /**
  * 退出登录
- *
+ * 
+ * TODO: 需要在迁移后重新实现
+ * 原因: 依赖 logout API 和路由跳转逻辑
+ * 推荐: 在组件中使用 Composition API 实现
  */
 // export function quiteLoginOut () {
 //   uni.showModal({
@@ -354,7 +360,6 @@ export function isLogin (val) {
 //     async success (res) {
 //       if (res.confirm) {
 //         storage.setAccessToken("");
-//         // storage.setRefreshToken("");
 //         storage.setUserInfo({});
 //         storage.setHasLogin(false)
 //         navigateToLogin("redirectTo");
@@ -366,7 +371,10 @@ export function isLogin (val) {
 
 /**
  * 用户注销
- *
+ * 
+ * TODO: 需要在迁移后重新实现
+ * 原因: 依赖 logoffConfirm API 和 getCurrentInstance
+ * 推荐: 在组件中使用 Composition API 实现
  */
 // export function logoff () {
 //   uni.showModal({
@@ -385,9 +393,12 @@ export function isLogin (val) {
 //   });
 // }
 
-
 /**
  * 跳转im
+ * 
+ * TODO: 需要在迁移后重新实现
+ * 原因: 依赖 isLogin 和路由跳转逻辑
+ * 推荐: 在组件中使用 Composition API 实现
  */
 /* export function talkIm (storeId, goodsId, id) {
   if (isLogin('auth')) {
@@ -402,6 +413,13 @@ export function isLogin (val) {
   }
 } */
 
+/**
+ * 提示登录
+ * 
+ * TODO: 需要在迁移后重新实现
+ * 原因: 依赖 getCurrentInstance 和路由跳转逻辑
+ * 推荐: 在组件中使用 Composition API 实现
+ */
 /* export function tipsToLogin (type) {
   if (!isLogin("auth")) {
     uni.showModal({
@@ -428,13 +446,20 @@ export function isLogin (val) {
 
 /**
  * 获取用户信息并重新添加到缓存里面
+ * 
+ * TODO: 需要在迁移后重新实现
+ * 原因: 依赖 getUserInfo API
+ * 推荐: 在组件中使用 Composition API 实现
  */
 export async function userInfo () {
-  let res = await getUserInfo();
-  if (res.data.success) {
-    storage.setUserInfo(res.data.result);
-    return res.data.result;
-  }
+  // TODO: 需要导入 getUserInfo API
+  // const res = await getUserInfo();
+  // if (res.data.success) {
+  //   storage.setUserInfo(res.data.result);
+  //   return res.data.result;
+  // }
+  console.warn('userInfo function needs to be implemented after migration');
+  return null;
 }
 
 /**
@@ -446,7 +471,7 @@ export async function userInfo () {
 export function forceLogin () {
   let userInfo = storage.getUserInfo();
   if (!userInfo || !userInfo.id) {
-    // #ifdef MP-WEIXIN
+    // #ifdef MP-WEIXIN || MP-HARMONY
 
     uni.navigateTo({
       url: "/pages/passport/wechatMPLogin",
@@ -454,7 +479,7 @@ export function forceLogin () {
 
     // #endif
 
-    // #ifndef MP-WEIXIN
+    // #ifndef MP-WEIXIN || MP-HARMONY
 
     uni.navigateTo({
       url: "/pages/passport/login",
@@ -485,12 +510,12 @@ export function navigateToLogin (type = "navigateTo") {
    * 微信小程序跳转到微信小程序登录页面
    * H5/App跳转到普通登录页面
    */
-  // #ifdef MP-WEIXIN
+  // #ifdef MP-WEIXIN || MP-HARMONY
   uni[type]({
     url: "/pages/passport/wechatMPLogin",
   });
   // #endif
-  // #ifndef MP-WEIXIN
+  // #ifndef MP-WEIXIN || MP-HARMONY
   uni[type]({
     url: "/pages/login/index",
   });

+ 21 - 4
utils/jessibuca-plugin.js

@@ -1,10 +1,12 @@
 /**
  * Jessibuca视频播放器插件
  * 用于初始化Jessibuca所需的脚本和依赖
+ * Vue 3 版本 - 使用条件编译支持跨平台
  */
 
 function loadJessibucaScript() {
   return new Promise((resolve, reject) => {
+    // #ifdef H5
     if (typeof window === 'undefined') {
       return reject(new Error('非浏览器环境,无法加载Jessibuca'))
     }
@@ -14,7 +16,7 @@ function loadJessibucaScript() {
     }
 
     const script = document.createElement('script')
-    script.src = './static/js/jessibuca/jessibuca.js' // 修改路径为相对路径
+    script.src = './static/js/jessibuca/jessibuca.js'
     script.async = true
 
     script.onload = () => {
@@ -43,17 +45,30 @@ function loadJessibucaScript() {
     }
 
     document.head.appendChild(script)
+    // #endif
+    
+    // #ifndef H5
+    // 非H5平台不支持Jessibuca
+    reject(new Error('Jessibuca仅支持H5平台'))
+    // #endif
   })
 }
 
 // 判断是否支持 Jessibuca(H5环境)
 function isSupported() {
+  // #ifdef H5
   return typeof window !== 'undefined' && typeof document !== 'undefined'
+  // #endif
+  
+  // #ifndef H5
+  return false
+  // #endif
 }
 
 export default {
-  install(Vue) {
-    Vue.prototype.$jessibuca = {
+  install(app) {
+    // Vue 3 使用 app.config.globalProperties 替代 Vue.prototype
+    app.config.globalProperties.$jessibuca = {
       loadScript: loadJessibucaScript,
       createPlayer(options) {
         return loadJessibucaScript().then((Jessibuca) => {
@@ -73,7 +88,8 @@ export default {
       isSupported
     }
 
-    // 预加载脚本(H5环境下执行)
+    // 预加载脚本(仅H5环境下执行)
+    // #ifdef H5
     if (isSupported()) {
       setTimeout(() => {
         loadJessibucaScript().catch(err => {
@@ -81,5 +97,6 @@ export default {
         })
       }, 1000)
     }
+    // #endif
   }
 }

+ 2 - 2
utils/lib/request/adapters/index.js

@@ -45,7 +45,7 @@ export default (config) => {
         name: config.name
       }
       const optionalKeys = [
-        // #ifdef APP-PLUS || H5
+        // #ifdef APP-PLUS || H5 || MP-HARMONY
         'files',
         // #endif
         // #ifdef H5
@@ -60,7 +60,7 @@ export default (config) => {
       const optionalKeys = [
         'data',
         'method',
-        // #ifdef MP-ALIPAY || MP-WEIXIN
+        // #ifdef MP-ALIPAY || MP-WEIXIN || MP-HARMONY
         'timeout',
         // #endif
         'dataType',

+ 5 - 5
utils/lib/request/core/Request.js

@@ -116,7 +116,7 @@ export default class Request {
 
   // #endif
 
-  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
+  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU || MP-HARMONY
   delete(url, data, options = {}) {
     return this.request({
       url,
@@ -128,7 +128,7 @@ export default class Request {
 
   // #endif
 
-  // #ifdef APP-PLUS || H5 || MP-WEIXIN
+  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-HARMONY
   connect(url, data, options = {}) {
     return this.request({
       url,
@@ -140,7 +140,7 @@ export default class Request {
 
   // #endif
 
-  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
+  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU || MP-HARMONY
   head(url, data, options = {}) {
     return this.request({
       url,
@@ -152,7 +152,7 @@ export default class Request {
 
   // #endif
 
-  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
+  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU || MP-HARMONY
   options(url, data, options = {}) {
     return this.request({
       url,
@@ -164,7 +164,7 @@ export default class Request {
 
   // #endif
 
-  // #ifdef APP-PLUS || H5 || MP-WEIXIN
+  // #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-HARMONY
   trace(url, data, options = {}) {
     return this.request({
       url,

+ 1 - 1
utils/lib/request/core/defaults.js

@@ -12,7 +12,7 @@ export default {
   responseType: 'text',
   // #endif
   custom: {},
-  // #ifdef MP-ALIPAY || MP-WEIXIN
+  // #ifdef MP-ALIPAY || MP-WEIXIN || MP-HARMONY
   timeout: 30000,
   // #endif
   // #ifdef APP-PLUS

+ 2 - 2
utils/lib/request/core/mergeConfig.js

@@ -55,7 +55,7 @@ export default (globalsConfig, config2 = {}) => {
       delete config.header['Content-Type']
     }
     const uploadKeys = [
-      // #ifdef APP-PLUS || H5
+      // #ifdef APP-PLUS || H5 || MP-HARMONY
       'files',
       // #endif
       // #ifdef MP-ALIPAY
@@ -76,7 +76,7 @@ export default (globalsConfig, config2 = {}) => {
   } else {
     const defaultsKeys = [
       'data',
-      // #ifdef MP-ALIPAY || MP-WEIXIN
+      // #ifdef MP-ALIPAY || MP-WEIXIN || MP-HARMONY
       'timeout',
       // #endif
       'dataType',

+ 22 - 0
utils/mixins/dictMixin.js

@@ -1,3 +1,25 @@
+/**
+ * 字典数据加载Mixin (Vue 2 Options API)
+ * 
+ * ⚠️ DEPRECATED: 此文件为 Vue2 版本,仅用于向后兼容
+ * 新代码请使用 Vue3 Composition API 版本: @/utils/composables/useDict.js
+ * 
+ * 迁移指南:
+ * Vue2 (Options API):
+ *   mixins: [dictMixin],
+ *   data() { return { dictTypeList: ['sys_user_sex'] } }
+ * 
+ * Vue3 (Composition API):
+ *   import { useDict } from '@/utils/composables/useDict'
+ *   const { dictData, loadDict, getDictLabel } = useDict(['sys_user_sex'])
+ * 
+ * 使用方式:
+ * 1. 在组件中导入 import dictMixin from '@/utils/mixins/dictMixin';
+ * 2. 在组件的mixins选项中注册 mixins: [dictMixin]
+ * 3. 在组件的data中定义需要的字典类型 dictTypeList: ['sys_user_sex', 'sys_normal_disable', ...]
+ * 4. 在组件的methods中调用 getDictLabel 等方法使用字典数据
+ * 5. 在模板中直接使用 dictData 对象获取字典项 v-for="item in dictData.sys_user_sex"
+ */
 import {
 	getDictData,
 	getMultipleDictData

+ 4 - 2
utils/request.js

@@ -9,6 +9,8 @@ import storage from "@/utils/storage.js";
 
 import jwt from '@/utils/js_sdk/t-jwt/jwt.js';
 import uuid from "@/utils/uuid.modified.js";
+// Vue3: Store is already migrated to use createStore from Vuex 4.x
+// Direct state access (store.state.xxx) is still valid in Vuex 4.x
 import store from "../store";
 
 
@@ -34,12 +36,12 @@ function cleanStorage() {
 	/* if (!isNavigateTo) {
 		isNavigateTo = true
 		// 防抖处理跳转
-		// #ifdef MP-WEIXIN
+		// #ifdef MP-WEIXIN || MP-HARMONY
 		uni.navigateTo({
 			url: "/pages/login/index",
 		});
 		// #endif
-		// #ifndef MP-WEIXIN
+		// #ifndef MP-WEIXIN || MP-HARMONY
 		uni.navigateTo({
 			url: "/pages/passport/login",
 		});

+ 13 - 3
utils/uuid.modified.js

@@ -1,4 +1,4 @@
-// uuid.js (适用于 Vue 3 的模块格式)
+// uuid.js (适用于 Vue 3 和 uni-app 的模块格式)
 'use strict';
 
 const _byteToHex = [];
@@ -18,8 +18,11 @@ function mathRNG() {
   return rnds;
 }
 
-// try crypto
+// 使用 uni-app 跨平台方式获取随机数生成器
 let rng = mathRNG;
+
+// #ifdef H5
+// H5环境下尝试使用 crypto API
 try {
   const _crypto = globalThis.crypto || globalThis.msCrypto;
   if (_crypto && _crypto.getRandomValues) {
@@ -30,8 +33,15 @@ try {
     };
   }
 } catch (e) {
-  // fallback already set
+  // fallback to mathRNG
+  console.log('Crypto API not available, using Math.random()');
 }
+// #endif
+
+// #ifndef H5
+// 非H5环境使用 Math.random()
+rng = mathRNG;
+// #endif
 
 // UUID v4
 function v4(options = {}, buf, offset = 0) {

+ 71 - 0
vite.config.js

@@ -0,0 +1,71 @@
+import { defineConfig } from 'vite'
+import uni from '@dcloudio/vite-plugin-uni'
+import path from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig(({ command, mode }) => {
+  const isApp = process.env.UNI_PLATFORM === 'app' || process.env.UNI_PLATFORM === 'app-harmony'
+  
+  return {
+  plugins: [uni()],
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, './'),
+      // 添加 uview-plus 别名
+      'uview-plus': path.resolve(__dirname, './node_modules/uview-plus')
+    }
+  },
+  optimizeDeps: {
+    include: ['uview-plus']
+  },
+  css: {
+    preprocessorOptions: {
+      scss: {
+        // 可以在这里添加全局 scss 变量
+        // 取消sass废弃API的报警
+		    silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'],  
+      }
+    }
+  },
+server: {
+    proxy: {
+      // H5 开发环境代理配置
+      '/base': {
+        target: 'http://localhost:8080',
+        changeOrigin: true,
+        secure: false,
+        rewrite: (path) => path.replace(/^\/base/, ''),
+      },
+      '/uniapp': {
+        target: 'http://localhost:8080',
+        changeOrigin: true,
+        secure: false,
+        rewrite: (path) => path.replace(/^\/base/, ''),
+      },
+    },
+  },
+  build: {
+    // 生产环境移除 console
+    minify: 'terser',
+    terserOptions: {
+      compress: {
+        drop_console: true,
+        drop_debugger: true
+      }
+    },
+    // App 平台(包括鸿蒙)禁用代码分割,避免 iife 格式错误
+    rollupOptions: isApp ? {
+      output: {
+        format: 'es',
+        inlineDynamicImports: true, // 关键:禁用代码分割
+        manualChunks: undefined
+      }
+    } : {
+      output: {
+        format: 'es',
+        manualChunks: undefined
+      }
+    },
+    target: 'esnext'
+  }
+}})

+ 9 - 0
vue.config.js

@@ -16,5 +16,14 @@ module.exports = {
             ]
             return args
         })
+    },
+    // Vue 3 编译器选项
+    transpileDependencies: ['uview-plus'],
+    configureWebpack: {
+        resolve: {
+            alias: {
+                'uview-plus': '@/node_modules/uview-plus'
+            }
+        }
     }
 }

+ 150 - 0
环境配置说明.md

@@ -0,0 +1,150 @@
+# 环境配置说明
+
+## 问题背景
+
+Vite 的 proxy 代理功能只在 **H5 开发环境** 下生效,对于 App、微信小程序、鸿蒙等平台不支持代理,必须直接使用真实域名。
+
+## 解决方案
+
+通过环境变量 + 条件编译的方式,实现不同平台使用不同的 API 地址。
+
+## 配置文件说明
+
+### 1. 环境变量文件
+
+- **`.env`** - 默认配置(所有环境的基础配置)
+- **`.env.development`** - 开发环境配置
+- **`.env.production`** - 生产环境配置
+
+### 2. 各平台 API 地址规则
+
+| 平台 | 环境 | API 地址 | 说明 |
+|------|------|----------|------|
+| H5 | 开发 | `/base` | 通过 Vite proxy 代理到 `http://localhost:8080` |
+| H5 | 生产 | `https://nxy.gbdfarm.com:9000/pro-uniapp` | 真实域名 |
+| 小程序 | 所有 | `https://nxy.gbdfarm.com:9000/pro-uniapp` | 真实域名 |
+| App | 所有 | `https://nxy.gbdfarm.com:9000/pro-uniapp` | 真实域名 |
+| 鸿蒙 | 所有 | `https://nxy.gbdfarm.com:9000/pro-uniapp` | 真实域名 |
+
+## 工作原理
+
+### H5 开发环境
+```javascript
+// 请求: /base/api/user/info
+// ↓ Vite proxy 转发
+// 实际请求: http://localhost:8080/api/user/info
+```
+
+### 其他平台或生产环境
+```javascript
+// 请求: https://nxy.gbdfarm.com:9000/pro-uniapp/api/user/info
+// ↓ 直接请求
+// 实际请求: https://nxy.gbdfarm.com:9000/pro-uniapp/api/user/info
+```
+
+## 如何使用
+
+### 开发环境
+
+1. **H5 开发**
+   ```bash
+   npm run dev:h5
+   ```
+   自动使用 `/base` 代理路径
+
+2. **小程序/App/鸿蒙开发**
+   ```bash
+   npm run dev:mp-weixin  # 微信小程序
+   npm run dev:app        # App
+   ```
+   自动使用真实域名
+
+### 生产环境
+
+```bash
+npm run build:h5           # H5 生产
+npm run build:mp-weixin    # 微信小程序生产
+npm run build:app          # App 生产
+```
+
+所有平台都使用真实域名
+
+## 修改配置
+
+如需修改 API 地址,编辑对应的环境变量文件:
+
+### 修改开发环境地址
+编辑 `.env.development`:
+```env
+VITE_BASE_URL=/base
+VITE_UPLOAD_URL=http://nxy.gbdfarm.com
+```
+
+### 修改生产环境地址
+编辑 `.env.production`:
+```env
+VITE_BASE_URL=https://nxy.gbdfarm.com:9000/pro-uniapp
+VITE_UPLOAD_URL=https://nxy.gbdfarm.com
+```
+
+## 注意事项
+
+1. **环境变量必须以 `VITE_` 开头**才能在代码中访问
+2. **修改环境变量后需要重启开发服务器**
+3. **小程序/App 不支持 proxy**,开发时也会直接请求真实域名
+4. **确保后端服务器配置了 CORS**,允许跨域请求
+
+## 技术实现
+
+### config/api.js
+使用条件编译判断平台:
+```javascript
+// #ifdef H5
+const isH5 = true;
+// #endif
+// #ifndef H5
+const isH5 = false;
+// #endif
+
+const dev = {
+  serve: isH5 ? "/base" : "https://nxy.gbdfarm.com:9000/pro-uniapp",
+  upload: import.meta.env.VITE_UPLOAD_URL
+};
+```
+
+### vite.config.js
+配置 H5 开发环境代理:
+```javascript
+server: {
+  proxy: {
+    '/base': {
+      target: 'http://localhost:8080',
+      changeOrigin: true,
+      rewrite: (path) => path.replace(/^\/base/, ''),
+    },
+  },
+}
+```
+
+## 常见问题
+
+### Q: 为什么小程序开发时不能用 proxy?
+A: 小程序运行在微信客户端,不是浏览器环境,无法使用 Vite 的开发服务器代理功能。
+
+### Q: 如何在本地调试小程序?
+A: 需要启动本地后端服务,或者使用内网穿透工具(如 ngrok)将本地服务暴露到公网。
+
+### Q: 环境变量不生效怎么办?
+A: 
+1. 检查变量名是否以 `VITE_` 开头
+2. 重启开发服务器
+3. 清除缓存后重新构建
+
+## 相关文件
+
+- `vite.config.js` - Vite 配置文件(包含 proxy 配置)
+- `config/api.js` - API 地址配置
+- `utils/request.js` - 请求拦截器
+- `.env` - 默认环境变量
+- `.env.development` - 开发环境变量
+- `.env.production` - 生产环境变量

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است