huoyi 1 dienu atpakaļ
revīzija
95d36a75ac
100 mainītis faili ar 41973 papildinājumiem un 0 dzēšanām
  1. 9 0
      .claude/settings.local.json
  2. 36 0
      .gitignore
  3. 3 0
      .gitmodules
  4. 81 0
      babel.config.js
  5. 44 0
      electron-builder.json
  6. 180 0
      electron/main.js
  7. 330 0
      nginx-files/select-airport-custom.html
  8. 27917 0
      package-lock.json
  9. 154 0
      package.json
  10. 27 0
      postcss.config.js
  11. 25 0
      public/index.html
  12. 11 0
      shims-uni.d.ts
  13. 4 0
      shims-vue.d.ts
  14. 16 0
      src/.gitignore
  15. 42 0
      src/App.vue
  16. 21 0
      src/LICENSE
  17. 51 0
      src/README.md
  18. 17 0
      src/api/announcement/announcement.js
  19. 79 0
      src/api/approve/approve.js
  20. 115 0
      src/api/attendance/attendance.js
  21. 11 0
      src/api/attendanceStatistics/attendanceStatistics.js
  22. 13 0
      src/api/check/checkProject.js
  23. 47 0
      src/api/check/checkReward.js
  24. 46 0
      src/api/check/checkTask.js
  25. 78 0
      src/api/check/checklist.js
  26. 101 0
      src/api/eikonStatistics/eikonStatistics.js
  27. 103 0
      src/api/exam/dailyExam.js
  28. 76 0
      src/api/exam/exam.js
  29. 141 0
      src/api/home-new/home-new.js
  30. 92 0
      src/api/inspectionStatistics/inspectionStatistics.js
  31. 47 0
      src/api/login-clould.js
  32. 59 0
      src/api/login.js
  33. 35 0
      src/api/myToDoList/myToDoList.js
  34. 28 0
      src/api/problemRect/problemRect.js
  35. 212 0
      src/api/qualityControlAnalysisReport/qualityControlAnalysisReport.js
  36. 46 0
      src/api/questionStatistics/questionStatistics.js
  37. 54 0
      src/api/seizeStatistics/seizeStatistics.js
  38. 38 0
      src/api/seizure/seizureRecord.js
  39. 64 0
      src/api/seizureRecord/seizureRecord.js
  40. 37 0
      src/api/statisticalAnalysis/statisticalAnalysis.js
  41. 14 0
      src/api/system/common.js
  42. 31 0
      src/api/system/dept/dept.js
  43. 52 0
      src/api/system/dict/data.js
  44. 60 0
      src/api/system/dict/type.js
  45. 128 0
      src/api/system/user.js
  46. 24 0
      src/api/voiceSubmissionDraft/voiceSubmissionDraft.js
  47. 18 0
      src/api/workDocu/workDocu.js
  48. 83 0
      src/api/workProfile/workProfile.js
  49. 39 0
      src/components/HeadTitle.vue
  50. 96 0
      src/components/HomeContainer.vue
  51. 52 0
      src/components/UserInfo.vue
  52. 149 0
      src/components/approve-history/approve-history.vue
  53. 150 0
      src/components/custom-tabbar.vue
  54. 284 0
      src/components/fuzzy-select/fuzzy-select.vue
  55. 138 0
      src/components/h-collapse-item/h-collapse-item.vue
  56. 74 0
      src/components/h-legend/h-legend.vue
  57. 200 0
      src/components/h-line/h-line.vue
  58. 104 0
      src/components/h-rank-line/h-rank-line.vue
  59. 128 0
      src/components/h-search/h-search.vue
  60. 105 0
      src/components/h-tabs/h-tabs.vue
  61. 81 0
      src/components/list-card/list-card.vue
  62. 38 0
      src/components/list-icon/list-icon.vue
  63. 134 0
      src/components/list-user/list-user.vue
  64. 75 0
      src/components/no-data/no-data.vue
  65. 81 0
      src/components/select-tag/select-tag.vue
  66. 136 0
      src/components/statistic-table/statistic-table.vue
  67. 218 0
      src/components/text-switch/text-switch.vue
  68. 113 0
      src/components/text-tag/text-tag.vue
  69. 103 0
      src/components/timeline-record/TimelineRecord.vue
  70. 167 0
      src/components/uni-section/uni-section.vue
  71. 30 0
      src/config.js
  72. 23 0
      src/main.js
  73. 71 0
      src/manifest.json
  74. 16 0
      src/package.json
  75. 373 0
      src/pages.json
  76. 182 0
      src/pages/announcement/announcementDetail.vue
  77. 204 0
      src/pages/announcement/index.vue
  78. 182 0
      src/pages/announcement/noticeDetail.vue
  79. 733 0
      src/pages/attendance/components/AddAttendancePersonnelModal.vue
  80. 290 0
      src/pages/attendance/components/AttendanceControl.vue
  81. 594 0
      src/pages/attendance/components/MaintainAreaOrMemberModal.vue
  82. 275 0
      src/pages/attendance/components/SearchView.vue
  83. 340 0
      src/pages/attendance/components/SelectArea.vue
  84. 75 0
      src/pages/attendance/components/SelectData.vue
  85. 59 0
      src/pages/attendance/components/TimePicker.vue
  86. 55 0
      src/pages/attendance/components/UserAvatar.vue
  87. 514 0
      src/pages/attendance/components/WorkingGroup.vue
  88. 376 0
      src/pages/attendance/index.vue
  89. 214 0
      src/pages/attendance/stats.vue
  90. 78 0
      src/pages/attendanceStatistics/components/TableList.vue
  91. 549 0
      src/pages/attendanceStatistics/index.vue
  92. 1460 0
      src/pages/capabilityComparison/index.vue
  93. 0 0
      src/pages/capabilityComparison/能力对比
  94. 315 0
      src/pages/checklist/components/SelectPerson.vue
  95. 121 0
      src/pages/checklist/components/SubmitResult.vue
  96. 216 0
      src/pages/checklist/components/unqualified.vue
  97. 986 0
      src/pages/checklist/index.vue
  98. 43 0
      src/pages/common/textview/index.vue
  99. 34 0
      src/pages/common/webview/index.vue
  100. 0 0
      src/pages/daily-exam/answer/index.vue

+ 9 - 0
.claude/settings.local.json

@@ -0,0 +1,9 @@
1
+{
2
+  "permissions": {
3
+    "allow": [
4
+      "Read(//c/Users/linzo/IdeaProjects/airport-server-single/**)"
5
+    ],
6
+    "deny": [],
7
+    "ask": []
8
+  }
9
+}

+ 36 - 0
.gitignore

@@ -0,0 +1,36 @@
1
+.DS_Store
2
+node_modules/
3
+unpackage/
4
+dist/
5
+dist.zip
6
+dist_electron
7
+CLAUDE.md
8
+# local env files
9
+.env.local
10
+.env.*.local
11
+
12
+# Log files
13
+npm-debug.log*
14
+yarn-debug.log*
15
+yarn-error.log*
16
+
17
+# Editor directories and files
18
+.project
19
+.idea
20
+.vscode
21
+*.suo
22
+*.ntvs*
23
+*.njsproj
24
+*.sln
25
+*.sw*
26
+nginx-files/airport.conf
27
+nginx-files/CHANGES.md
28
+nginx-files/DEPLOYMENT_GUIDE.md
29
+nginx-files/DEPLOYMENT.md
30
+nginx-files/DIAGNOSE_HAIKOU.md
31
+nginx-files/FRONTEND_INTEGRATION.md
32
+nginx-files/HOTFIX.md
33
+nginx-files/nginx.conf.updated
34
+nginx-files/README_CUSTOM.md
35
+nginx-files/README.md
36
+nginx-files/select-airport.html

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
1
+[submodule "src"]
2
+	path = src
3
+	url = https://gitee.com/y_project/RuoYi-App.git

+ 81 - 0
babel.config.js

@@ -0,0 +1,81 @@
1
+const webpack = require('webpack')
2
+const plugins = []
3
+
4
+if (process.env.UNI_OPT_TREESHAKINGNG) {
5
+  plugins.push(require('@dcloudio/vue-cli-plugin-uni-optimize/packages/babel-plugin-uni-api/index.js'))
6
+}
7
+
8
+if (
9
+  (
10
+    process.env.UNI_PLATFORM === 'app-plus' &&
11
+    process.env.UNI_USING_V8
12
+  ) ||
13
+  (
14
+    process.env.UNI_PLATFORM === 'h5' &&
15
+    process.env.UNI_H5_BROWSER === 'builtin'
16
+  )
17
+) {
18
+  const path = require('path')
19
+
20
+  const isWin = /^win/.test(process.platform)
21
+
22
+  const normalizePath = path => (isWin ? path.replace(/\\/g, '/') : path)
23
+
24
+  const input = normalizePath(process.env.UNI_INPUT_DIR)
25
+  try {
26
+    plugins.push([
27
+      require('@dcloudio/vue-cli-plugin-hbuilderx/packages/babel-plugin-console'),
28
+      {
29
+        file (file) {
30
+          file = normalizePath(file)
31
+          if (file.indexOf(input) === 0) {
32
+            return path.relative(input, file)
33
+          }
34
+          return false
35
+        }
36
+      }
37
+    ])
38
+  } catch (e) { }
39
+}
40
+
41
+process.UNI_LIBRARIES = process.UNI_LIBRARIES || ['@dcloudio/uni-ui']
42
+process.UNI_LIBRARIES.forEach(libraryName => {
43
+  plugins.push([
44
+    'import',
45
+    {
46
+      'libraryName': libraryName,
47
+      'customName': (name) => {
48
+        return `${libraryName}/lib/${name}/${name}`
49
+      }
50
+    }
51
+  ])
52
+})
53
+
54
+if (process.env.UNI_PLATFORM !== 'h5') {
55
+  plugins.push('@babel/plugin-transform-runtime')
56
+}
57
+
58
+const config = {
59
+  presets: [
60
+    [
61
+      '@vue/app',
62
+      {
63
+        modules: webpack.version[0] > 4 ? 'auto' : 'commonjs',
64
+        useBuiltIns: process.env.UNI_PLATFORM === 'h5' ? 'usage' : 'entry'
65
+      }
66
+    ]
67
+  ],
68
+  plugins
69
+}
70
+
71
+const UNI_H5_TEST = '**/@dcloudio/uni-h5/dist/index.umd.min.js'
72
+if (process.env.NODE_ENV === 'production') {
73
+  config.overrides = [{
74
+    test: UNI_H5_TEST,
75
+    compact: true,
76
+  }]
77
+} else {
78
+  config.ignore = [UNI_H5_TEST]
79
+}
80
+
81
+module.exports = config

+ 44 - 0
electron-builder.json

@@ -0,0 +1,44 @@
1
+{
2
+  "appId": "com.meilan.app",
3
+  "productName": "美兰机场应用",
4
+  "directories": {
5
+    "output": "dist_electron",
6
+    "buildResources": "build"
7
+  },
8
+  "files": [
9
+    "dist/build/h5/**/*",
10
+    "electron/**/*",
11
+    "!**/node_modules/**/*"
12
+  ],
13
+  "extraResources": [
14
+    {
15
+      "from": "dist/build/h5",
16
+      "to": "app/dist/build/h5",
17
+      "filter": ["**/*"]
18
+    }
19
+  ],
20
+  "mac": {
21
+    "category": "public.app-category.productivity",
22
+    "icon": "dist/build/h5/static/pc.png"
23
+  },
24
+  "win": {
25
+    "target": [
26
+      {
27
+        "target": "nsis",
28
+        "arch": ["x64"]
29
+      }
30
+    ],
31
+    "icon": "dist/build/h5/static/pc.png"
32
+  },
33
+  "nsis": {
34
+    "oneClick": false,
35
+    "allowToChangeInstallationDirectory": true,
36
+    "createDesktopShortcut": true,
37
+    "createStartMenuShortcut": true,
38
+    "shortcutName": "美兰机场应用"
39
+  },
40
+  "publish": {
41
+    "provider": "generic",
42
+    "url": "http://your-update-server.com/updates"
43
+  }
44
+}

+ 180 - 0
electron/main.js

@@ -0,0 +1,180 @@
1
+const { app, BrowserWindow, Menu } = require('electron')
2
+const path = require('path')
3
+const isDev = process.env.NODE_ENV === 'development'
4
+
5
+// 保持对窗口对象的全局引用,避免被垃圾回收
6
+let mainWindow
7
+
8
+function createWindow() {
9
+  // 创建浏览器窗口
10
+  mainWindow = new BrowserWindow({
11
+    width: 375, // 手机宽度(类似iPhone 12/13)
12
+    height: 667, // 手机高度
13
+    minWidth: 320, // 最小宽度(类似小屏手机)
14
+    minHeight: 480, // 最小高度
15
+    maxWidth: 414, // 最大宽度(类似大屏手机)
16
+    maxHeight: 896, // 最大高度
17
+    webPreferences: {
18
+      nodeIntegration: false,
19
+      contextIsolation: true,
20
+      webSecurity: false, // 禁用webSecurity以允许跨域请求
21
+      allowRunningInsecureContent: true, // 允许不安全内容
22
+      enableRemoteModule: false, // 禁用远程模块,提高安全性
23
+      webgl: true,
24
+      images: true
25
+    },
26
+    icon: path.join(__dirname, '../../dist/build/h5/static/pc.png'), // 应用图标
27
+    show: false // 先隐藏窗口,等加载完成再显示
28
+  })
29
+
30
+  // 设置Content Security Policy
31
+  mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
32
+    callback({
33
+      responseHeaders: {
34
+        ...details.responseHeaders,
35
+        'Content-Security-Policy': [
36
+          "default-src * 'unsafe-inline' 'unsafe-eval' data: blob:; " +
37
+          "script-src * 'unsafe-inline' 'unsafe-eval'; " +
38
+          "connect-src * 'unsafe-inline'; " +
39
+          "img-src * data: blob: 'unsafe-inline'; " +
40
+          "frame-src *; " +
41
+          "style-src * 'unsafe-inline'; " +
42
+          "font-src * data:; " +
43
+          "media-src *"
44
+        ]
45
+      }
46
+    })
47
+  })
48
+
49
+  // 加载应用
50
+  if (isDev) {
51
+    // 开发环境:加载本地H5开发服务器
52
+    mainWindow.loadURL('http://localhost:9090')
53
+    // 打开开发者工具
54
+    mainWindow.webContents.openDevTools()
55
+  } else {
56
+    // 生产环境:加载打包后的文件
57
+    const fs = require('fs')
58
+    let indexPath
59
+    
60
+    // 尝试多种可能的路径
61
+    const possiblePaths = [
62
+      // 打包后路径1:resources/app/dist/build/h5
63
+      process.resourcesPath ? path.join(process.resourcesPath, 'app', 'dist', 'build', 'h5', 'index.html') : null,
64
+      // 打包后路径2:resources/app
65
+      process.resourcesPath ? path.join(process.resourcesPath, 'app', 'index.html') : null,
66
+      // 打包后路径3:直接位于应用目录
67
+      path.join(__dirname, '..', 'dist', 'build', 'h5', 'index.html'),
68
+      // 开发环境路径
69
+      path.join(__dirname, '..', '..', 'dist', 'build', 'h5', 'index.html'),
70
+      // 备用路径
71
+      path.join(__dirname, 'dist', 'build', 'h5', 'index.html')
72
+    ].filter(Boolean)
73
+    
74
+    // 查找存在的文件
75
+    for (const testPath of possiblePaths) {
76
+      console.log('Testing path:', testPath)
77
+      if (fs.existsSync(testPath)) {
78
+        indexPath = testPath
79
+        console.log('Found index file at:', indexPath)
80
+        break
81
+      }
82
+    }
83
+    
84
+    if (indexPath && fs.existsSync(indexPath)) {
85
+      // 使用 loadFile 方法加载文件
86
+      mainWindow.loadFile(indexPath)
87
+      console.log('Successfully loaded index file from:', indexPath)
88
+    } else {
89
+      console.error('Index file not found in any of the following paths:')
90
+      possiblePaths.forEach(p => console.error('  -', p))
91
+      
92
+      // 显示详细的错误信息
93
+      const errorHtml = `
94
+        <html>
95
+          <head><title>应用加载失败</title></head>
96
+          <body style="font-family: Arial, sans-serif; padding: 20px;">
97
+            <h1>应用文件加载失败</h1>
98
+            <p>无法找到 index.html 文件,请检查以下路径:</p>
99
+            <ul>
100
+              ${possiblePaths.map(p => `<li>${p}</li>`).join('')}
101
+            </ul>
102
+            <p>请重新安装应用或联系技术支持。</p>
103
+          </body>
104
+        </html>
105
+      `
106
+      mainWindow.loadURL(`data:text/html,${encodeURIComponent(errorHtml)}`)
107
+    }
108
+  }
109
+
110
+  // 窗口准备好后显示
111
+  mainWindow.once('ready-to-show', () => {
112
+    mainWindow.show()
113
+    // 设置页面缩放以适应手机尺寸
114
+    mainWindow.webContents.setZoomFactor(0.85) // 85%缩放,让内容更紧凑
115
+  })
116
+
117
+  // 窗口关闭时触发
118
+  mainWindow.on('closed', () => {
119
+    mainWindow = null
120
+  })
121
+
122
+  // 设置菜单(可选)
123
+  const template = [
124
+    // {
125
+    //   label: '文件',
126
+    //   submenu: [
127
+    //     {
128
+    //       label: '退出',
129
+    //       accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q',
130
+    //       click() {
131
+    //         app.quit()
132
+    //       }
133
+    //     }
134
+    //   ]
135
+    // },
136
+    // {
137
+    //   label: '视图',
138
+    //   submenu: [
139
+    //     { role: 'reload', label: '重新加载' },
140
+    //     { role: 'forceReload', label: '强制重新加载' },
141
+    //     { role: 'toggleDevTools', label: '开发者工具' },
142
+    //     { type: 'separator' },
143
+    //     { role: 'resetZoom', label: '实际大小' },
144
+    //     { role: 'zoomIn', label: '放大' },
145
+    //     { role: 'zoomOut', label: '缩小' },
146
+    //     { type: 'separator' },
147
+    //     { role: 'togglefullscreen', label: '切换全屏' }
148
+    //   ]
149
+    // }
150
+  ]
151
+
152
+  const menu = Menu.buildFromTemplate(template)
153
+  Menu.setApplicationMenu(menu)
154
+}
155
+
156
+// Electron 初始化完成时触发
157
+app.whenReady().then(createWindow)
158
+
159
+// 所有窗口关闭时退出应用(macOS除外)
160
+app.on('window-all-closed', () => {
161
+  if (process.platform !== 'darwin') {
162
+    app.quit()
163
+  }
164
+})
165
+
166
+// macOS 应用激活时重新创建窗口
167
+app.on('activate', () => {
168
+  if (BrowserWindow.getAllWindows().length === 0) {
169
+    createWindow()
170
+  }
171
+})
172
+
173
+// 安全设置:阻止新窗口创建
174
+app.on('web-contents-created', (event, contents) => {
175
+  contents.on('new-window', (event, navigationUrl) => {
176
+    event.preventDefault()
177
+    // 在默认浏览器中打开外部链接
178
+    require('electron').shell.openExternal(navigationUrl)
179
+  })
180
+})

+ 330 - 0
nginx-files/select-airport-custom.html

@@ -0,0 +1,330 @@
1
+<!DOCTYPE html>
2
+<html lang="zh-CN">
3
+<head>
4
+    <meta charset="UTF-8">
5
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+    <meta name="apple-mobile-web-app-capable" content="yes">
7
+    <meta name="apple-mobile-web-app-status-bar-style" content="black">
8
+    <title>选择机场 - 安检分级质控系统</title>
9
+    <style>
10
+        * {
11
+            margin: 0;
12
+            padding: 0;
13
+            box-sizing: border-box;
14
+        }
15
+
16
+        body {
17
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
18
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
+            min-height: 100vh;
20
+            display: flex;
21
+            align-items: center;
22
+            justify-content: center;
23
+            padding: 20px;
24
+        }
25
+
26
+        .container {
27
+            width: 100%;
28
+            max-width: 400px;
29
+        }
30
+
31
+        .logo-section {
32
+            text-align: center;
33
+            margin-bottom: 40px;
34
+        }
35
+
36
+        .logo {
37
+            width: 80px;
38
+            height: 80px;
39
+            background: white;
40
+            border-radius: 20px;
41
+            margin: 0 auto 20px;
42
+            display: flex;
43
+            align-items: center;
44
+            justify-content: center;
45
+            font-size: 40px;
46
+            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
47
+        }
48
+
49
+        .title {
50
+            color: white;
51
+            font-size: 24px;
52
+            font-weight: 600;
53
+            margin-bottom: 8px;
54
+        }
55
+
56
+        .subtitle {
57
+            color: rgba(255, 255, 255, 0.8);
58
+            font-size: 14px;
59
+        }
60
+
61
+        .card {
62
+            background: white;
63
+            border-radius: 16px;
64
+            padding: 30px 20px;
65
+            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
66
+        }
67
+
68
+        .card-title {
69
+            font-size: 18px;
70
+            font-weight: 600;
71
+            color: #333;
72
+            margin-bottom: 20px;
73
+            text-align: center;
74
+        }
75
+
76
+        .airport-list {
77
+            list-style: none;
78
+        }
79
+
80
+        .airport-item {
81
+            background: #f7f8fa;
82
+            border-radius: 12px;
83
+            padding: 16px;
84
+            margin-bottom: 12px;
85
+            cursor: pointer;
86
+            transition: all 0.3s ease;
87
+            display: flex;
88
+            align-items: center;
89
+            border: 2px solid transparent;
90
+        }
91
+
92
+        .airport-item:hover {
93
+            background: #e8ecf4;
94
+            transform: translateY(-2px);
95
+            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
96
+        }
97
+
98
+        .airport-item:active {
99
+            transform: translateY(0);
100
+        }
101
+
102
+        .airport-item.selected {
103
+            border-color: #667eea;
104
+            background: #f0f3ff;
105
+        }
106
+
107
+        .airport-icon {
108
+            width: 40px;
109
+            height: 40px;
110
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
111
+            border-radius: 10px;
112
+            display: flex;
113
+            align-items: center;
114
+            justify-content: center;
115
+            font-size: 20px;
116
+            margin-right: 15px;
117
+            flex-shrink: 0;
118
+        }
119
+
120
+        .airport-info {
121
+            flex: 1;
122
+        }
123
+
124
+        .airport-name {
125
+            font-size: 16px;
126
+            font-weight: 600;
127
+            color: #333;
128
+            margin-bottom: 4px;
129
+        }
130
+
131
+        .airport-desc {
132
+            font-size: 12px;
133
+            color: #999;
134
+        }
135
+
136
+        .arrow {
137
+            color: #ccc;
138
+            font-size: 20px;
139
+        }
140
+
141
+        .airport-item.selected .arrow {
142
+            color: #667eea;
143
+        }
144
+
145
+        .footer {
146
+            text-align: center;
147
+            margin-top: 20px;
148
+            color: rgba(255, 255, 255, 0.6);
149
+            font-size: 12px;
150
+        }
151
+
152
+        .loading {
153
+            display: none;
154
+            text-align: center;
155
+            margin-top: 20px;
156
+        }
157
+
158
+        .loading.show {
159
+            display: block;
160
+        }
161
+
162
+        .loading-spinner {
163
+            width: 30px;
164
+            height: 30px;
165
+            border: 3px solid rgba(255, 255, 255, 0.3);
166
+            border-radius: 50%;
167
+            border-top-color: white;
168
+            animation: spin 1s ease-in-out infinite;
169
+            margin: 0 auto 10px;
170
+        }
171
+
172
+        @keyframes spin {
173
+            to { transform: rotate(360deg); }
174
+        }
175
+
176
+        .loading-text {
177
+            color: white;
178
+            font-size: 14px;
179
+        }
180
+
181
+        /* 移动端优化 */
182
+        @media (max-width: 480px) {
183
+            .title {
184
+                font-size: 20px;
185
+            }
186
+
187
+            .card {
188
+                padding: 24px 16px;
189
+            }
190
+
191
+            .airport-item {
192
+                padding: 14px;
193
+            }
194
+        }
195
+    </style>
196
+</head>
197
+<body>
198
+    <div class="container">
199
+        <div class="logo-section">
200
+            <div class="logo">✈️</div>
201
+            <div class="title">安检分级质控系统</div>
202
+            <div class="subtitle">Airport Security Quality Control</div>
203
+        </div>
204
+
205
+        <div class="card">
206
+            <div class="card-title">请选择您的机场</div>
207
+            <ul class="airport-list" id="airportList">
208
+                <!-- 动态生成 -->
209
+            </ul>
210
+        </div>
211
+
212
+        <div class="loading" id="loading">
213
+            <div class="loading-spinner"></div>
214
+            <div class="loading-text">正在跳转...</div>
215
+        </div>
216
+
217
+        <div class="footer">
218
+            © 2025 机场安检质控系统
219
+        </div>
220
+    </div>
221
+
222
+    <script>
223
+        // 机场配置列表(根据实际环境配置)
224
+        const airports = [
225
+            {
226
+                code: 'SHUANGLIU',
227
+                name: '双流机场',
228
+                description: '成都双流国际机场',
229
+                icon: '🛫'
230
+            },
231
+            {
232
+                code: 'HAIKOU',
233
+                name: '海口机场',
234
+                description: '海口美兰国际机场',
235
+                icon: '✈️'
236
+            }
237
+            // 可以继续添加更多机场
238
+            // {
239
+            //     code: 'ANOTHER_AIRPORT',
240
+            //     name: '其他机场',
241
+            //     description: '其他机场描述',
242
+            //     icon: '🛬'
243
+            // }
244
+        ];
245
+
246
+        let selectedCode = null;
247
+
248
+        // 渲染机场列表
249
+        function renderAirports() {
250
+            const listEl = document.getElementById('airportList');
251
+            listEl.innerHTML = airports.map(airport => `
252
+                <li class="airport-item" data-code="${airport.code}" onclick="selectAirport('${airport.code}')">
253
+                    <div class="airport-icon">${airport.icon}</div>
254
+                    <div class="airport-info">
255
+                        <div class="airport-name">${airport.name}</div>
256
+                        <div class="airport-desc">${airport.description}</div>
257
+                    </div>
258
+                    <div class="arrow">›</div>
259
+                </li>
260
+            `).join('');
261
+        }
262
+
263
+        // 选择机场
264
+        function selectAirport(code) {
265
+            selectedCode = code;
266
+
267
+            // 更新UI
268
+            document.querySelectorAll('.airport-item').forEach(item => {
269
+                item.classList.remove('selected');
270
+            });
271
+            document.querySelector(`[data-code="${code}"]`).classList.add('selected');
272
+
273
+            // 延迟跳转,让用户看到选中效果
274
+            setTimeout(() => {
275
+                confirmSelection(code);
276
+            }, 300);
277
+        }
278
+
279
+        // 确认选择并跳转
280
+        function confirmSelection(code) {
281
+            // 显示加载动画
282
+            document.getElementById('loading').classList.add('show');
283
+
284
+            // 设置 Cookie(30天有效期)
285
+            const maxAge = 30 * 24 * 60 * 60; // 30天
286
+            document.cookie = `airport_code=${code}; path=/; max-age=${maxAge}; SameSite=Lax`;
287
+
288
+            // 同时设置 localStorage(备用方案)
289
+            try {
290
+                localStorage.setItem('airport_code', code);
291
+            } catch (e) {
292
+                console.warn('无法设置 localStorage:', e);
293
+            }
294
+
295
+            // 延迟跳转到首页
296
+            setTimeout(() => {
297
+                window.location.href = '/';
298
+            }, 500);
299
+        }
300
+
301
+        // 检查是否已有选择
302
+        function checkExistingSelection() {
303
+            // 从 cookie 读取
304
+            const cookies = document.cookie.split(';');
305
+            for (let cookie of cookies) {
306
+                const [name, value] = cookie.trim().split('=');
307
+                if (name === 'airport_code') {
308
+                    // 已有选择,自动跳转
309
+                    window.location.href = '/';
310
+                    return;
311
+                }
312
+            }
313
+        }
314
+
315
+        // 初始化
316
+        document.addEventListener('DOMContentLoaded', () => {
317
+            checkExistingSelection();
318
+            renderAirports();
319
+        });
320
+
321
+        // 防止iOS端的bounce效果
322
+        document.addEventListener('touchmove', (e) => {
323
+            if (e.target.closest('.airport-list')) {
324
+                return;
325
+            }
326
+            e.preventDefault();
327
+        }, { passive: false });
328
+    </script>
329
+</body>
330
+</html>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 27917 - 0
package-lock.json


+ 154 - 0
package.json

@@ -0,0 +1,154 @@
1
+{
2
+  "name": "ruoyi-app-project",
3
+  "version": "1.0.0",
4
+  "description": "美兰机场移动应用 - 基于uni-app开发的跨平台应用",
5
+  "author": "美兰机场技术团队",
6
+  "private": true,
7
+  "scripts": {
8
+    "dev": "npm run dev:h5",
9
+    "build": "npm run build:h5",
10
+    "build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
11
+    "build:custom": "cross-env NODE_ENV=production uniapp-cli custom",
12
+    "build:h5": "cross-env NODE_ENV=production UNI_PLATFORM=h5 vue-cli-service uni-build",
13
+    "electron:dev": "cross-env NODE_ENV=development electron electron/main.js",
14
+    "electron:build": "npm run build:h5 && electron-builder",
15
+    "electron:pack": "npm run build:h5 && electron-builder --dir",
16
+    "electron:dist": "npm run build:h5 && electron-builder --win --x64",
17
+    "build:mp-360": "cross-env NODE_ENV=production UNI_PLATFORM=mp-360 vue-cli-service uni-build",
18
+    "build:mp-alipay": "cross-env NODE_ENV=production UNI_PLATFORM=mp-alipay vue-cli-service uni-build",
19
+    "build:mp-baidu": "cross-env NODE_ENV=production UNI_PLATFORM=mp-baidu vue-cli-service uni-build",
20
+    "build:mp-jd": "cross-env NODE_ENV=production UNI_PLATFORM=mp-jd vue-cli-service uni-build",
21
+    "build:mp-kuaishou": "cross-env NODE_ENV=production UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build",
22
+    "build:mp-lark": "cross-env NODE_ENV=production UNI_PLATFORM=mp-lark vue-cli-service uni-build",
23
+    "build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build",
24
+    "build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build",
25
+    "build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
26
+    "build:mp-xhs": "cross-env NODE_ENV=production UNI_PLATFORM=mp-xhs vue-cli-service uni-build",
27
+    "build:quickapp-native": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-native vue-cli-service uni-build",
28
+    "build:quickapp-webview": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview vue-cli-service uni-build",
29
+    "build:quickapp-webview-huawei": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build",
30
+    "build:quickapp-webview-union": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build",
31
+    "dev:app-plus": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-build --watch",
32
+    "dev:custom": "cross-env NODE_ENV=development uniapp-cli custom",
33
+    "dev:h5": "cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve",
34
+    "dev:mp-360": "cross-env NODE_ENV=development UNI_PLATFORM=mp-360 vue-cli-service uni-build --watch",
35
+    "dev:mp-alipay": "cross-env NODE_ENV=development UNI_PLATFORM=mp-alipay vue-cli-service uni-build --watch",
36
+    "dev:mp-baidu": "cross-env NODE_ENV=development UNI_PLATFORM=mp-baidu vue-cli-service uni-build --watch",
37
+    "dev:mp-jd": "cross-env NODE_ENV=development UNI_PLATFORM=mp-jd vue-cli-service uni-build --watch",
38
+    "dev:mp-kuaishou": "cross-env NODE_ENV=development UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build --watch",
39
+    "dev:mp-lark": "cross-env NODE_ENV=development UNI_PLATFORM=mp-lark vue-cli-service uni-build --watch",
40
+    "dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch",
41
+    "dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch",
42
+    "dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
43
+    "dev:mp-xhs": "cross-env NODE_ENV=development UNI_PLATFORM=mp-xhs vue-cli-service uni-build --watch",
44
+    "dev:quickapp-native": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-native vue-cli-service uni-build --watch",
45
+    "dev:quickapp-webview": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview vue-cli-service uni-build --watch",
46
+    "dev:quickapp-webview-huawei": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build --watch",
47
+    "dev:quickapp-webview-union": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build --watch",
48
+    "info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js",
49
+    "serve:quickapp-native": "node node_modules/@dcloudio/uni-quickapp-native/bin/serve.js",
50
+    "test:android": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=android jest -i",
51
+    "test:h5": "cross-env UNI_PLATFORM=h5 jest -i",
52
+    "test:ios": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=ios jest -i",
53
+    "test:mp-baidu": "cross-env UNI_PLATFORM=mp-baidu jest -i",
54
+    "test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"
55
+  },
56
+  "dependencies": {
57
+    "@dcloudio/uni-app": "^2.0.2-3090920231225001",
58
+    "@dcloudio/uni-app-plus": "^2.0.2-3090920231225001",
59
+    "@dcloudio/uni-h5": "^2.0.2-3090920231225001",
60
+    "@dcloudio/uni-i18n": "^2.0.2-3090920231225001",
61
+    "@dcloudio/uni-mp-360": "^2.0.2-3090920231225001",
62
+    "@dcloudio/uni-mp-alipay": "^2.0.2-3090920231225001",
63
+    "@dcloudio/uni-mp-baidu": "^2.0.2-3090920231225001",
64
+    "@dcloudio/uni-mp-jd": "^2.0.2-3090920231225001",
65
+    "@dcloudio/uni-mp-kuaishou": "^2.0.2-3090920231225001",
66
+    "@dcloudio/uni-mp-lark": "^2.0.2-3090920231225001",
67
+    "@dcloudio/uni-mp-qq": "^2.0.2-3090920231225001",
68
+    "@dcloudio/uni-mp-toutiao": "^2.0.2-3090920231225001",
69
+    "@dcloudio/uni-mp-vue": "^2.0.2-3090920231225001",
70
+    "@dcloudio/uni-mp-weixin": "^2.0.2-3090920231225001",
71
+    "@dcloudio/uni-mp-xhs": "^2.0.2-3090920231225001",
72
+    "@dcloudio/uni-quickapp-native": "^2.0.2-3090920231225001",
73
+    "@dcloudio/uni-quickapp-webview": "^2.0.2-3090920231225001",
74
+    "@dcloudio/uni-stacktracey": "^2.0.2-3090920231225001",
75
+    "@dcloudio/uni-stat": "^2.0.2-3090920231225001",
76
+    "@vue/shared": "^3.0.0",
77
+    "core-js": "^3.8.3",
78
+    "echarts": "^5.6.0",
79
+    "flyio": "^0.6.2",
80
+    "moment": "^2.30.1",
81
+    "postcss": "^8.4.35",
82
+    "uview-ui": "^2.0.38",
83
+    "vue": ">= 2.6.14 < 2.7",
84
+    "vuex": "^3.2.0"
85
+  },
86
+  "devDependencies": {
87
+    "@dcloudio/types": "^3.3.2",
88
+    "@dcloudio/uni-automator": "^2.0.2-3090920231225001",
89
+    "@dcloudio/uni-cli-i18n": "^2.0.2-3090920231225001",
90
+    "@dcloudio/uni-cli-shared": "^2.0.2-3090920231225001",
91
+    "@dcloudio/uni-helper-json": "*",
92
+    "@dcloudio/uni-migration": "^2.0.2-3090920231225001",
93
+    "@dcloudio/uni-template-compiler": "^2.0.2-3090920231225001",
94
+    "@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.2-3090920231225001",
95
+    "@dcloudio/vue-cli-plugin-uni": "^2.0.2-3090920231225001",
96
+    "@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.2-3090920231225001",
97
+    "@dcloudio/webpack-uni-mp-loader": "^2.0.2-3090920231225001",
98
+    "@dcloudio/webpack-uni-pages-loader": "^2.0.2-3090920231225001",
99
+    "@vue/cli-plugin-babel": "~5.0.0",
100
+    "@vue/cli-service": "~5.0.0",
101
+    "autoprefixer": "^8.0.0",
102
+    "babel-plugin-import": "^1.11.0",
103
+    "cross-env": "^7.0.2",
104
+    "electron": "^19.0.0",
105
+    "electron-builder": "^23.0.0",
106
+    "jest": "^25.4.0",
107
+    "postcss-comment": "^2.0.0",
108
+    "postcss-loader": "^8.1.1",
109
+    "sass": "^1.49.8",
110
+    "sass-loader": "^8.0.2",
111
+    "vue-template-compiler": ">= 2.6.14 < 2.7"
112
+  },
113
+  "browserslist": [
114
+    "Android >= 4.4",
115
+    "ios >= 9"
116
+  ],
117
+  "uni-app": {
118
+    "scripts": {}
119
+  },
120
+  "main": "electron/main.js",
121
+  "homepage": "./",
122
+  "build": {
123
+    "appId": "com.meilan.app",
124
+    "productName": "美兰机场应用",
125
+    "directories": {
126
+      "output": "dist_electron"
127
+    },
128
+    "electronDownload": {
129
+      "mirror": "https://npmmirror.com/mirrors/electron/"
130
+    },
131
+    "files": [
132
+      "dist/build/h5/**/*",
133
+      "electron/**/*"
134
+    ],
135
+    "win": {
136
+      "target": [
137
+        {
138
+          "target": "nsis",
139
+          "arch": [
140
+            "x64"
141
+          ]
142
+        }
143
+      ],
144
+      "icon": "dist/build/h5/static/pc.png"
145
+    },
146
+    "nsis": {
147
+      "oneClick": false,
148
+      "allowToChangeInstallationDirectory": true,
149
+      "createDesktopShortcut": true,
150
+      "createStartMenuShortcut": true,
151
+      "shortcutName": "美兰机场应用"
152
+    }
153
+  }
154
+}

+ 27 - 0
postcss.config.js

@@ -0,0 +1,27 @@
1
+const path = require('path')
2
+const webpack = require('webpack')
3
+const config = {
4
+  parser: require('postcss-comment'),
5
+  plugins: [
6
+    require('postcss-import')({
7
+      resolve (id, basedir, importOptions) {
8
+        if (id.startsWith('~@/')) {
9
+          return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
10
+        } else if (id.startsWith('@/')) {
11
+          return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
12
+        } else if (id.startsWith('/') && !id.startsWith('//')) {
13
+          return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
14
+        }
15
+        return id
16
+      }
17
+    }),
18
+    require('autoprefixer')({
19
+      remove: process.env.UNI_PLATFORM !== 'h5'
20
+    }),
21
+    require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
22
+  ]
23
+}
24
+if (webpack.version[0] > 4) {
25
+  delete config.parser
26
+}
27
+module.exports = config

+ 25 - 0
public/index.html

@@ -0,0 +1,25 @@
1
+<!DOCTYPE html>
2
+<html lang="zh-CN">
3
+
4
+    <head>
5
+        <meta charset="utf-8">
6
+        <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+        <title>
8
+            <%= htmlWebpackPlugin.options.title %>
9
+        </title>
10
+        <script>
11
+            var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
12
+            document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
13
+        </script>
14
+        <link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
15
+    </head>
16
+
17
+    <body>
18
+        <noscript>
19
+            <strong>Please enable JavaScript to continue.</strong>
20
+        </noscript>
21
+        <div id="app"></div>
22
+        <!-- built files will be auto injected -->
23
+    </body>
24
+
25
+</html>

+ 11 - 0
shims-uni.d.ts

@@ -0,0 +1,11 @@
1
+/// <reference types='@dcloudio/types' />
2
+import Vue from 'vue'
3
+declare module "vue/types/options" {
4
+  type Hooks = App.AppInstance & Page.PageInstance;
5
+  interface ComponentOptions<V extends Vue> extends Hooks {
6
+    /**
7
+     * 组件类型
8
+     */
9
+    mpType?: string;
10
+  }
11
+}

+ 4 - 0
shims-vue.d.ts

@@ -0,0 +1,4 @@
1
+declare module "*.vue" {
2
+  import Vue from 'vue'
3
+  export default Vue
4
+}

+ 16 - 0
src/.gitignore

@@ -0,0 +1,16 @@
1
+######################################################################
2
+# Build Tools
3
+
4
+/unpackage/*
5
+/node_modules/*
6
+
7
+######################################################################
8
+# Development Tools
9
+
10
+/.idea/*
11
+/.vscode/*
12
+/.hbuilderx/*
13
+
14
+package-lock.json
15
+yarn.lock
16
+

+ 42 - 0
src/App.vue

@@ -0,0 +1,42 @@
1
+<script>
2
+import config from './config'
3
+import { getToken } from '@/utils/auth'
4
+import { showMessageTabRedDot } from "@/utils/common.js"
5
+export default {
6
+  onLaunch: function () {
7
+    this.initApp()
8
+    // 在应用启动时为消息tab显示红点
9
+    showMessageTabRedDot()
10
+  },
11
+  methods: {
12
+    // 初始化应用
13
+    initApp() {
14
+      // 初始化应用配置
15
+      this.initConfig()
16
+      // 检查用户登录状态
17
+      //#ifdef H5
18
+      this.checkLogin()
19
+      //#endif
20
+    },
21
+    initConfig() {
22
+      this.globalData.config = config
23
+    },
24
+    checkLogin() {
25
+      if (!getToken()) {
26
+        this.$tab.reLaunch('/pages/login')
27
+      }
28
+    },
29
+  }
30
+}
31
+</script>
32
+
33
+<style lang="scss">
34
+@import "uview-ui/index.scss";
35
+@import '@/static/scss/index.scss';
36
+
37
+uni-page-body {
38
+  height: 100%;
39
+  margin-bottom: 0 !important;
40
+  padding-bottom: 0 !important;
41
+}
42
+</style>

+ 21 - 0
src/LICENSE

@@ -0,0 +1,21 @@
1
+MIT License
2
+
3
+Copyright (c) 2022 若依
4
+
5
+Permission is hereby granted, free of charge, to any person obtaining a copy
6
+of this software and associated documentation files (the "Software"), to deal
7
+in the Software without restriction, including without limitation the rights
8
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+copies of the Software, and to permit persons to whom the Software is
10
+furnished to do so, subject to the following conditions:
11
+
12
+The above copyright notice and this permission notice shall be included in all
13
+copies or substantial portions of the Software.
14
+
15
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+SOFTWARE.

+ 51 - 0
src/README.md

@@ -0,0 +1,51 @@
1
+<p align="center">
2
+	<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-43e3941654fa3054c9684bf53d1b1d356a1.png">
3
+</p>
4
+<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi v1.2.0</h1>
5
+<h4 align="center">基于UniApp开发的轻量级移动端框架</h4>
6
+<p align="center">
7
+	<a href="https://gitee.com/y_project/RuoYi-App/stargazers"><img src="https://gitee.com/y_project/RuoYi-App/badge/star.svg?theme=dark"></a>
8
+	<a href="https://gitee.com/y_project/RuoYi-App"><img src="https://img.shields.io/badge/RuoYi-v1.2.0-brightgreen.svg"></a>
9
+	<a href="https://gitee.com/y_project/RuoYi-App/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
10
+</p>
11
+
12
+## 平台简介
13
+
14
+RuoYi App 移动解决方案,采用uniapp框架,一份代码多终端适配,同时支持APP、小程序、H5!实现了与[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue)、[RuoYi-Cloud](https://gitee.com/y_project/RuoYi-Cloud)完美对接的移动解决方案!目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。
15
+
16
+* 配套后端代码仓库地址[RuoYi-Vue](https://gitee.com/y_project/RuoYi-Vue) 或 [RuoYi-Cloud](https://github.com/yangzongzhuan/RuoYi-Cloud) 版本。
17
+* 应用框架基于[uniapp](https://uniapp.dcloud.net.cn/),支持小程序、H5、Android和IOS。
18
+* 前端组件采用[uni-ui](https://github.com/dcloudio/uni-ui),全端兼容的高性能UI框架。
19
+* 阿里云折扣场:[点我进入](http://aly.ruoyi.vip),腾讯云秒杀场:[点我进入](http://txy.ruoyi.vip)&nbsp;&nbsp;
20
+
21
+
22
+## 技术文档
23
+
24
+- 官网网站:[http://ruoyi.vip](http://ruoyi.vip)
25
+- 文档地址:[http://doc.ruoyi.vip](http://doc.ruoyi.vip)
26
+- H5页体验:[http://h5.ruoyi.vip](http://h5.ruoyi.vip)
27
+- QQ交流群: ①133713780(满)、②146013835(满)、③189091635
28
+- 小程序体验
29
+
30
+<img src="https://oscimg.oschina.net/oscnet/up-26c76dc90b92acdbd9ac8cd5252f07c8ad9.jpg" alt="小程序演示"/>
31
+ 
32
+
33
+## 演示图
34
+
35
+<table>
36
+    <tr>
37
+        <td><img src="https://oscimg.oschina.net/oscnet/up-21f6f842fdc94540469b4eb43fdadbaf7f8.png"/></td>
38
+        <td><img src="https://oscimg.oschina.net/oscnet/up-a6f23cf9a371a30165e135eff6d9ae89a9d.png"/></td>
39
+		<td><img src="https://oscimg.oschina.net/oscnet/up-ff5f62016bf6624c1ff27eee57499dccd44.png"/></td>
40
+    </tr>
41
+	<tr>
42
+        <td><img src="https://oscimg.oschina.net/oscnet/up-b9a582fdb26ec69d407fabd044d2c8494df.png"/></td>
43
+        <td><img src="https://oscimg.oschina.net/oscnet/up-96427ee08fca29d77934cfc8d1b1a637cef.png"/></td>
44
+		<td><img src="https://oscimg.oschina.net/oscnet/up-5fdadc582d24cccd7727030d397b63185a3.png"/></td>
45
+    </tr>
46
+	<tr>
47
+        <td><img src="https://oscimg.oschina.net/oscnet/up-0a36797b6bcc50c36d40c3c782665b89efc.png"/></td>
48
+        <td><img src="https://oscimg.oschina.net/oscnet/up-d77995cc00687cedd00d5ac7d68a07ea276.png"/></td>
49
+		<td><img src="https://oscimg.oschina.net/oscnet/up-fa8f5ab20becf59b4b38c1b92a9989e7109.png"/></td>
50
+    </tr>
51
+</table>

+ 17 - 0
src/api/announcement/announcement.js

@@ -0,0 +1,17 @@
1
+import request from '@/utils/request'
2
+
3
+//获取通知公告列表
4
+export const getNoticeList = (data) => {
5
+    return request({
6
+        url: `/system/notice/list`,
7
+        method: 'get',
8
+        data
9
+    })
10
+}
11
+//获取公告详情
12
+export const getNoticeDetail = (id) => {
13
+    return request({
14
+        url: `/system/notice/${id}`,
15
+        method: 'get'
16
+    })
17
+}

+ 79 - 0
src/api/approve/approve.js

@@ -0,0 +1,79 @@
1
+import request from '@/utils/request'
2
+//查询审批历史
3
+export const getApprovelHistory = (instanceId) => {
4
+    return request({
5
+        url: `/system/check/approval/history/instance/${instanceId}`,
6
+        method: 'get',
7
+    })
8
+}
9
+
10
+//根据用户查部门负责人
11
+export const selectDeptLeaderByUserId = (data) => {
12
+    return request({
13
+        url: `/system/user/selectDeptLeaderByUserId`,
14
+        method: 'get',
15
+        data
16
+    })
17
+}
18
+
19
+//审批通过
20
+export const approvePass = (taskId,data) => {
21
+    return request({
22
+        url: `/system/approval/approve/${taskId}`,
23
+        method: 'put',
24
+        data
25
+    })
26
+}
27
+
28
+//审批驳回
29
+export const approveReject = (taskId,data) => {
30
+    return request({
31
+        url: `/system/approval/reject/${taskId}`,
32
+        method: 'put',
33
+        data
34
+    })
35
+}
36
+
37
+//批量审批通过
38
+export const approvePassBatch = (data) => {
39
+    return request({
40
+        url: `/system/approval/approve/list`,
41
+        method: 'put',
42
+        data
43
+    })
44
+}
45
+
46
+//批量审批驳回
47
+export const approveRejectBatch = (data) => {
48
+    return request({
49
+        url: `/system/approval/reject/list`,
50
+        method: 'put',
51
+        data
52
+    })
53
+}
54
+
55
+//查询审批列表
56
+export const getApproveList = (data) => {
57
+    return request({
58
+        url: `/system/approval/reject/list`,
59
+        method: 'get',
60
+        data
61
+    })
62
+}
63
+
64
+//整改审批通过
65
+export const rectifyApprovePass = (data) => {
66
+    return request({
67
+        url: `/check/checkCorrection/approveTaskBatch`,
68
+        method: 'post',
69
+        data
70
+    })
71
+}
72
+//整改审批驳回
73
+export const rectifyApproveReject = (data) => {
74
+    return request({
75
+        url: `/check/checkCorrection/rejectTaskBatch`,
76
+        method: 'post',
77
+        data
78
+    })
79
+}

+ 115 - 0
src/api/attendance/attendance.js

@@ -0,0 +1,115 @@
1
+import request from '@/utils/request'
2
+
3
+// 保存打卡
4
+export function addAttendance(data) {
5
+  return request({
6
+    url: '/attendance/v1/record',
7
+    method: 'post',
8
+    data: data
9
+  })
10
+}
11
+
12
+export function getAttendanceList(data) {
13
+  return request({
14
+    url: '/attendance/v1/record-list',
15
+    method: 'post',
16
+    data: data
17
+  })
18
+}
19
+
20
+// 获取同组人员列表
21
+export function getUserList(params) {
22
+  return request({
23
+    url: '/system/user/list',
24
+    method: 'get',
25
+    params: params
26
+  })
27
+}
28
+
29
+// 添加上岗记录
30
+export function addPostRecord (data) {
31
+  return request({
32
+    url: '/attendance/postRecord/direct',
33
+    method: 'post',
34
+    data: data
35
+  })
36
+}
37
+
38
+// 新增:获取上岗记录列表
39
+export function getPostRecordList (params) {
40
+  return request({
41
+    url: '/attendance/postRecord/list',
42
+    method: 'get',
43
+    params: params
44
+  })
45
+}
46
+// 获取班组上岗列表
47
+export function listgroupbyTimeanduserid () {
48
+  return request({
49
+    url: `/attendance/postRecord/listgroupbyTimeanduserid`,
50
+    method: 'get'
51
+  })
52
+}
53
+
54
+// 新增:更新上岗记录
55
+export function updatePostRecord (data) {
56
+  return request({
57
+    url: '/attendance/postRecord/direct',
58
+    method: 'put',
59
+    data: data
60
+  })
61
+}
62
+
63
+// 查询通道和区域
64
+export function dataConfigTree (level) {
65
+  return request({
66
+    url: '/dataConfig/dataConfigTree',
67
+    method: 'get',
68
+    data: {
69
+      treeType: 'POSITION',
70
+      maxLevel: level
71
+    }
72
+  })
73
+}
74
+// 查询通道和区域
75
+export function areaList () {
76
+  return request({
77
+    url: '/attendance/area/list',
78
+    method: 'post'
79
+  })
80
+}
81
+
82
+//查询班组成员
83
+export function memberList (data) {
84
+  return request({
85
+    url: '/attendance/record/list',
86
+    method: 'get',
87
+    data: data
88
+  })
89
+}
90
+
91
+//批量新增考勤班组成员
92
+export function addMember (data) {
93
+  return request({
94
+    url: '/attendance/record/add/list',
95
+    method: 'post',
96
+    data
97
+  })
98
+}
99
+
100
+//查询最后一次上岗区域
101
+export function queryLastTime (data) {
102
+  return request({
103
+    url: '/attendance/postRecord/query-last-time',
104
+    method: 'get',
105
+    data: data
106
+  })
107
+}
108
+
109
+//查询上班打卡和下班打卡是否可以点击
110
+export function queryClickAble () {
111
+  return request({
112
+    url: '/attendance/v1/getRecordType',
113
+    method: 'get',
114
+  })
115
+}

+ 11 - 0
src/api/attendanceStatistics/attendanceStatistics.js

@@ -0,0 +1,11 @@
1
+import request from '@/utils/request'
2
+
3
+
4
+// 获取通道统计
5
+export function getChannelStatistics(status) {
6
+  return request({
7
+    url: '/attendance/postRecord/count?regionalStatus='+status,
8
+    method: 'post',
9
+    
10
+  })
11
+}

+ 13 - 0
src/api/check/checkProject.js

@@ -0,0 +1,13 @@
1
+import request from '@/utils/request'
2
+
3
+// 获取检查项
4
+export function getCheckProject(parentId) {
5
+  return request({
6
+    url: '/system/checkCategory/listTree',
7
+    // headers: {
8
+    //   isToken: false
9
+    // },
10
+    method: 'get',
11
+    params: {parentId}
12
+  })
13
+}

+ 47 - 0
src/api/check/checkReward.js

@@ -0,0 +1,47 @@
1
+import request from '@/utils/request'
2
+
3
+// 提交检查单
4
+export function submitInspection(data) {
5
+  return request({
6
+    url: '/check/checkRecord/add',
7
+    method: 'post',
8
+    data: data
9
+  })
10
+}
11
+
12
+// 获取检查单列表
13
+
14
+export function getInspectionList(params = {}) {
15
+  return request({
16
+    url: '/check/checkRecord/list',
17
+    method: 'get',
18
+    params
19
+  });
20
+}
21
+
22
+//获取检查记录详细信息
23
+export function getInspectionListById(id) {
24
+  return request({
25
+    url: `/check/checkRecord/${id}`,   //  反引号
26
+    method: 'get',
27
+  });
28
+}
29
+
30
+//新增草稿检查记录
31
+
32
+export function addDraftInspection(data) {
33
+  return request({
34
+    url: '/check/checkRecord/draft',
35
+    method: 'post',
36
+    data: data
37
+  })
38
+}
39
+
40
+//修改检查记录
41
+export function updateInspection(data) {
42
+  return request({
43
+    url: '/check/checkRecord',
44
+    method: 'put',
45
+    data: data
46
+  })
47
+}

+ 46 - 0
src/api/check/checkTask.js

@@ -0,0 +1,46 @@
1
+import request from '@/utils/request'
2
+
3
+// 查询检查任务列表
4
+export function listCheckTask(query) {
5
+  return request({
6
+    url: '/check/checkTask/list',
7
+    method: 'get',
8
+    params: query
9
+  })
10
+}
11
+//新增检查任务
12
+export function addCheckTask(data) {
13
+  return request({
14
+    url: '/check/checkTask',
15
+    method: 'post',
16
+    data: data
17
+  })
18
+}
19
+//获取检查任务详细信息
20
+export function getCheckTask(id) {
21
+  return request({
22
+    url: '/check/checkTask/' + id,
23
+    method: 'get'
24
+  })
25
+}
26
+//删除检查任务
27
+export function delCheckTask(id) {
28
+  return request({
29
+    url: '/check/checkTask/' + id,
30
+    method: 'delete'
31
+  })
32
+}
33
+//根据被检查级别查询检查项目列表
34
+export function getCheckProjectItemList(checkLevel) {
35
+  return request({
36
+    url: '/system/project/listByCheckLevel/' + checkLevel,
37
+    method: 'get'
38
+  })
39
+}
40
+//获取未读数量
41
+export function getUnreadCount() {
42
+  return request({
43
+    url: '/check/checkTask/unReadNum',
44
+    method: 'get'
45
+  })
46
+}

+ 78 - 0
src/api/check/checklist.js

@@ -0,0 +1,78 @@
1
+import request from '@/utils/request'
2
+
3
+// 新增检查单记录
4
+export function addChecklistRecord(data) {
5
+  return request({
6
+    url: '/check/checklist/add',
7
+    method: 'post',
8
+    data: data
9
+  })
10
+}
11
+
12
+// 获取检查单列表
13
+export function getChecklistList(params) {
14
+  return request({
15
+    url: '/check/checklist/list',
16
+    method: 'get',
17
+    params: params
18
+  })
19
+}
20
+
21
+// 获取检查单详情
22
+export function getChecklistDetail(id) {
23
+  return request({
24
+    url: `/check/checklist/detail/${id}`,
25
+    method: 'get'
26
+  })
27
+}
28
+
29
+// 更新检查单
30
+export function updateChecklist(data) {
31
+  return request({
32
+    url: '/check/checklist/update',
33
+    method: 'put',
34
+    data: data
35
+  })
36
+}
37
+
38
+// 删除检查单
39
+export function deleteChecklist(id) {
40
+  return request({
41
+    url: `/check/checklist/delete/${id}`,
42
+    method: 'delete'
43
+  })
44
+}
45
+
46
+// 提交审批
47
+export function submitApproval(data) {
48
+  return request({
49
+    url: '/check/checklist/submitApproval',
50
+    method: 'post',
51
+    data: data
52
+  })
53
+}
54
+
55
+// 获取审批历史
56
+export function getApprovalHistory(checklistId) {
57
+  return request({
58
+    url: `/check/checklist/approvalHistory/${checklistId}`,
59
+    method: 'get'
60
+  })
61
+}
62
+
63
+// 获取检查单统计
64
+export function getChecklistStatistics(params) {
65
+  return request({
66
+    url: '/check/checklist/statistics',
67
+    method: 'get',
68
+    params: params
69
+  })
70
+}
71
+//查询部门指定角色人员
72
+export function getDeptRoleUser(deptId,data) {
73
+  return request({
74
+    url: `/system/dept/deptRole/${deptId}`,
75
+    method: 'post',
76
+    data: data
77
+  })
78
+}

+ 101 - 0
src/api/eikonStatistics/eikonStatistics.js

@@ -0,0 +1,101 @@
1
+import request from '@/utils/request'
2
+
3
+// 查获取指定模块的指标值
4
+export function getModuleMetrics(params) {
5
+  return request({
6
+    url: '/user/basic/portrait/module/info',
7
+    method: 'get',
8
+    params: params
9
+  })
10
+}
11
+
12
+//获取用户在指定层级的详细排名信息
13
+export function getRankInfo(params) {
14
+  return request({
15
+    url: '/item/user-ranking/ranking-detail',
16
+    method: 'get',
17
+    params: params
18
+  })
19
+}
20
+
21
+//巡检画像
22
+export function getPortrait(params) {
23
+  return request({
24
+    url: '/check/largeScreen/portrait',
25
+    method: 'get',
26
+    params: params
27
+  })
28
+}
29
+//能力画像-学习成长
30
+export function getGrowthPortrait(params) {
31
+  return request({
32
+    url: '/system/growth/portrait',
33
+    method: 'get',
34
+    params: params
35
+  })
36
+}
37
+//总体概览接口
38
+export function getOverview(params) {
39
+  return request({
40
+    url: '/system/user/population',
41
+    method: 'get',
42
+    params: params
43
+  })
44
+}
45
+//获取指定用户画像
46
+export function getUserProfile(params) {
47
+  return request({
48
+    url: '/exam/daily/user-profile',
49
+    method: 'get',
50
+    params: params
51
+  })
52
+}
53
+
54
+//获取班组和科室画像
55
+export function getDeptProfile(params) {
56
+  return request({
57
+    url: '/exam/daily/dept-profile',
58
+    method: 'get',
59
+    params: params
60
+  })
61
+}
62
+//获取站级画像
63
+export function getSiteProfile(params) {
64
+  return request({
65
+    url: '/exam/daily/site-profile',
66
+    method: 'get',
67
+    params: params
68
+  })
69
+}
70
+//能力画像-协同配合
71
+export function getCollaborationProfile(params) {
72
+  return request({
73
+    url: '/system/user/cooperation',
74
+    method: 'get',
75
+    params: params
76
+  })
77
+}
78
+//能力画像-明细
79
+export function getDetailProfile(params) {
80
+  return request({
81
+    url: '/system/user/detail',
82
+    method: 'get',
83
+    params: params
84
+  })
85
+}
86
+//计算站级查获统计
87
+export function getSiteStatistics(params) {
88
+  return request({
89
+    url: `/item/user-ranking/station`,
90
+    method: 'get',
91
+    params: params
92
+  })
93
+}
94
+//计算站级考勤工作统计
95
+export function getAttendanceStatistics(params) {
96
+  return request({
97
+    url: `/attendance/stats/station`,
98
+    method: 'get',
99
+    params: params  
100
+  })
101
+}

+ 103 - 0
src/api/exam/dailyExam.js

@@ -0,0 +1,103 @@
1
+import request from '@/utils/request'
2
+
3
+// 查询我的任务列表(当天有效任务)
4
+export function getMyTasks(params) {
5
+  return request({
6
+    url: '/exam/daily/task/my-tasks-today',
7
+    method: 'get',
8
+    params: params
9
+  })
10
+}
11
+
12
+// 查询今日是否有待完成任务
13
+export function hasPendingTask() {
14
+  return request({
15
+    url: '/exam/daily/task/has-pending-task',
16
+    method: 'get'
17
+  })
18
+}
19
+
20
+// 查询任务详情
21
+export function getTaskDetail(taskId) {
22
+  return request({
23
+    url: `/exam/daily/task/${taskId}`,
24
+    method: 'get'
25
+  })
26
+}
27
+
28
+// 开始答题
29
+export function startTask(taskId) {
30
+  return request({
31
+    url: `/exam/daily/task/start/${taskId}`,
32
+    method: 'post'
33
+  })
34
+}
35
+
36
+// 提交答案
37
+export function submitAnswer(data) {
38
+  return request({
39
+    url: '/exam/daily/task/submit',
40
+    method: 'post',
41
+    data: data
42
+  })
43
+}
44
+
45
+// 获取任务剩余时间
46
+export function getRemainingTime(taskId) {
47
+  return request({
48
+    url: `/exam/daily/task/remaining-time/${taskId}`,
49
+    method: 'get'
50
+  })
51
+}
52
+
53
+// 获取我的统计数据
54
+export function getMyStatistics() {
55
+  return request({
56
+    url: '/exam/daily/statistics/my-statistics',
57
+    method: 'get'
58
+  })
59
+}
60
+
61
+// 获取我的薄弱模块
62
+export function getWeakModules() {
63
+  return request({
64
+    url: '/exam/daily/statistics/weak-modules',
65
+    method: 'get'
66
+  })
67
+}
68
+
69
+//已完成任务列表
70
+export function getCompletedTasks(params) {
71
+  return request({
72
+    url: '/exam/daily/task/my-tasks-completed',
73
+    method: 'get',
74
+    params: params
75
+  })
76
+}
77
+
78
+//已过期任务列表
79
+export function getExpiredTasks(params) {
80
+  return request({
81
+    url: '/exam/daily/task/my-tasks-expired',
82
+    method: 'get',
83
+    params: params
84
+  })
85
+}
86
+
87
+//全部任务列表
88
+export function getAllTasks(params) {
89
+  return request({
90
+    url: '/exam/daily/task/my-tasks',
91
+    method: 'get',
92
+    params: params
93
+  })
94
+}
95
+
96
+//待考试任务列表
97
+export function getPendingTasks(params) {
98
+  return request({
99
+    url: '/exam/daily/task/my-tasks-today',
100
+    method: 'get',
101
+    params: params
102
+  })
103
+}

+ 76 - 0
src/api/exam/exam.js

@@ -0,0 +1,76 @@
1
+import request from '@/utils/request'
2
+import { getToken } from '@/utils/auth'
3
+
4
+
5
+
6
+
7
+// 获取查获接口列表
8
+export function getExamIntro(data) {
9
+  return request({
10
+    url: '/v1/cs/app/eighteen/get_exam_intro',
11
+    method: 'post',
12
+    data: data
13
+  })
14
+}
15
+
16
+// 测试接口
17
+export function getExamRecordList(data) {
18
+  return request({
19
+    url: '/v1/cs/manage/exam/list',
20
+    method: 'post',
21
+    data: data
22
+  })
23
+}
24
+
25
+
26
+// 答题
27
+export function applyExam(data) {
28
+
29
+  return request({
30
+    url: '/v1/cs/app/eighteen/get_exam',
31
+    method: 'post',
32
+    data: data,
33
+  })
34
+}
35
+
36
+//提交试卷
37
+export function submitExam(data) {
38
+  return request({
39
+    url: '/v1/cs/app/eighteen/submit_exam',
40
+    method: 'post',
41
+    data: data
42
+  })
43
+}
44
+
45
+//查询测试记录
46
+export function getExamRecord(data) {
47
+  return request({
48
+    url: '/v1/cs/app/eighteen/get_exam',
49
+    method: 'post',
50
+    data: data
51
+  })
52
+}
53
+
54
+//获取答题结果
55
+export function getExam(data) {
56
+  return request({
57
+    url: '/v1/cs/app/eighteen/get_exam_record',
58
+    method: 'post',
59
+    data: data
60
+  })
61
+}
62
+
63
+//查询测试记录列表
64
+export function getUserExamRecordList(data) {
65
+  return request({
66
+    url: '/v1/cs/app/eighteen/get_exam_record_list',
67
+    method: 'post',
68
+    data: data
69
+  })
70
+}
71
+
72
+
73
+
74
+
75
+
76
+

+ 141 - 0
src/api/home-new/home-new.js

@@ -0,0 +1,141 @@
1
+import request from '@/utils/request'
2
+
3
+// 查询我的任务列表(当天有效任务)shudong
4
+export function getHomePage(params) {
5
+  return request({
6
+    url: '/check/largeScreen/homePage',
7
+    method: 'get',
8
+    params: params
9
+  })
10
+}
11
+//获取查获上报数据  xiaoxiong
12
+export function getSeizureReport(params) {
13
+  return request({
14
+    url: '/system/check/seizureReport/data',
15
+    method: 'get',
16
+    params: params
17
+  })
18
+}
19
+
20
+//获取考勤统计数据   binge
21
+export function getAttendanceStats(params) {
22
+  return request({
23
+    url: '/attendance/stats/getAttendanceStats',
24
+    method: 'get',
25
+    params: params
26
+  })
27
+}
28
+
29
+//抽问抽答,首页   binge
30
+export function getAccuracyStatistics(params) {
31
+  return request({
32
+    url: '/exam/daily/accuracy-statistics',
33
+    method: 'get',
34
+    params: params
35
+  })
36
+}
37
+
38
+//根据角色获取查获排名  xiaoxiong
39
+export function getSeizureRanking(data) {
40
+  return request({
41
+    url: '/item/seizure/ranking/getRankingByRole',
42
+    method: 'post',
43
+    data: data
44
+  })
45
+}
46
+
47
+
48
+//获取检查排名  shudong
49
+export function getCheckRanking(params) {
50
+  return request({
51
+    url: '/system/homePage/homePageRanking',
52
+    method: 'get',
53
+    params: params
54
+  })
55
+}
56
+
57
+//首页-整体   shudong binge xiaoxiong
58
+export function getHomePageWhole(params) {
59
+  return request({
60
+    url: '/system/homePage/homePageWhole',
61
+    method: 'get',
62
+    params: params
63
+  })
64
+}
65
+//首页-明细(能力对比) shudong binge xiaoxiong
66
+export function getHomePageDetail(data) {
67
+  return request({
68
+    url: '/system/homePage/homePageDetail',
69
+    method: 'post',
70
+    data: data
71
+  })
72
+}
73
+
74
+//根据角色标识查询今日上岗用户列表
75
+export function selectUserListByRoleKey(data) {
76
+  return request({
77
+    url: '/attendance/postRecord/selectUserListByRoleKey',
78
+    method: 'post',
79
+    data: data
80
+  })
81
+}
82
+
83
+//首页报表-整体
84
+export function getHomeReportWhole(params) {
85
+  return request({
86
+    url: '/system/homeReport/homeReportWhole',
87
+    method: 'get',
88
+    params: params
89
+  })
90
+}
91
+
92
+//绩效指标列表
93
+export function getMetrics(data) {
94
+  return request({
95
+    url: '/item/performance/metrics',
96
+    method: 'post',
97
+    data: data
98
+  })
99
+}
100
+
101
+//质控活动-问题分布统计
102
+export function getCheckProblemDistribution(query) {
103
+  return request({
104
+    url: '/system/analysisReport/checkProblemDistribution',
105
+    method: 'get',
106
+    params: query
107
+  })
108
+}
109
+
110
+//培训测试正确率分析
111
+export function getAccuracyAnalysis(query) {
112
+  return request({
113
+    url: '/exam/quiz/accuracy-analysis',
114
+    method: 'get',
115
+    params: query
116
+  })
117
+}
118
+//查询全站查获违禁品 TOP3
119
+export function getProhibitedTop3(query) {
120
+  return request({
121
+    url: '/item/performance/prohibited-top3',
122
+    method: 'get',
123
+    params: query
124
+  })
125
+}
126
+//查询隐匿重点部位 Top1
127
+export function getConcealmentPositionTop1(query) {
128
+  return request({
129
+    url: '/item/performance/concealment-position-top1',
130
+    method: 'get',
131
+    params: query
132
+  })
133
+}
134
+//获取查获消息推送数据
135
+export function getPushMessage(query) {
136
+  return request({
137
+    url: '/item/seizure/push/message',
138
+    method: 'get',
139
+    params: query
140
+  })
141
+}

+ 92 - 0
src/api/inspectionStatistics/inspectionStatistics.js

@@ -0,0 +1,92 @@
1
+import request from '@/utils/request'
2
+
3
+//巡检计划-计划安排总览
4
+export function getPlanOverview(params) {
5
+    return request({
6
+        url: '/check/largeScreen/planOverview',
7
+        method: 'get',
8
+        params: params
9
+    })
10
+}
11
+
12
+
13
+//巡检计划-日常任务检查指标累积分布
14
+export function getDailyTaskMetrics(params) {
15
+    return request({
16
+        url: '/check/largeScreen/planDistribution',
17
+        method: 'get',
18
+        params: params
19
+    })
20
+}
21
+
22
+//巡检计划-任务明细统计表
23
+export function getTaskDetailStats(params) {
24
+    return request({
25
+        url: '/check/largeScreen/planStatistics',
26
+        method: 'get',
27
+        params: params
28
+    })
29
+}
30
+
31
+//问题发现-总体问题分布
32
+export function getProblemDistribution(params) {
33
+    return request({
34
+        url: '/check/largeScreen/problemDistribution',
35
+        method: 'get',
36
+        params: params
37
+    })
38
+}
39
+
40
+//问题发现-问题分布对比
41
+export function getProblemDistributionComparison(params) {
42
+    return request({
43
+        url: '/check/largeScreen/problemComparison',
44
+        method: 'get',
45
+        params: params
46
+    })
47
+}
48
+
49
+//问题发现-通道面貌-问题对比
50
+export function getChannelProblemComparison(params) {
51
+    return request({
52
+        url: '/check/largeScreen/problemComparisonTwo',
53
+        method: 'get',
54
+        params: params
55
+    })
56
+}
57
+
58
+//问题发现-通道面貌-问题趋势
59
+export function getChannelProblemTrend(params) {
60
+    return request({
61
+        url: '/check/largeScreen/problemTrend',
62
+        method: 'get',
63
+        params: params
64
+    })
65
+}
66
+
67
+//问题整改-整改状态总计
68
+export function getRectificationStatusTotal(params) {
69
+    return request({
70
+        url: '/check/largeScreen/correction',
71
+        method: 'get',
72
+        params: params
73
+    })
74
+}
75
+
76
+//问题整改-整改状态分布-科级对比
77
+export function getRectificationStatusComparison(params) {
78
+    return request({
79
+        url: '/check/largeScreen/correctionDistribution',
80
+        method: 'get',
81
+        params: params
82
+    })
83
+}
84
+
85
+//巡检执行
86
+export function getExecutionStatusTotal(params) {
87
+    return request({
88
+        url: '/check/largeScreen/inspectionExecute',
89
+        method: 'get',
90
+        params: params
91
+    })
92
+}

+ 47 - 0
src/api/login-clould.js

@@ -0,0 +1,47 @@
1
+import request from '@/utils/request'
2
+
3
+// 登录方法
4
+export function login(username, password, code, uuid) {
5
+  const data = {
6
+    username,
7
+    password,
8
+    code,
9
+    uuid
10
+  }
11
+  return request({
12
+    'url': '/auth/login',
13
+    headers: {
14
+      isToken: false
15
+    },
16
+    'method': 'post',
17
+    'data': data
18
+  })
19
+}
20
+
21
+// 获取用户详细信息
22
+export function getInfo() {
23
+  return request({
24
+    'url': '/system/user/getInfo',
25
+    'method': 'get'
26
+  })
27
+}
28
+
29
+// 退出方法
30
+export function logout() {
31
+  return request({
32
+    'url': '/auth/logout',
33
+    'method': 'delete'
34
+  })
35
+}
36
+
37
+// 获取验证码
38
+export function getCodeImg() {
39
+  return request({
40
+    'url': '/code',
41
+    headers: {
42
+      isToken: false
43
+    },
44
+    method: 'get',
45
+    timeout: 20000
46
+  })
47
+}

+ 59 - 0
src/api/login.js

@@ -0,0 +1,59 @@
1
+import request from '@/utils/request'
2
+
3
+// 登录方法
4
+export function login(username, password, code, uuid) {
5
+  const data = {
6
+    username,
7
+    password,
8
+    code,
9
+    uuid
10
+  }
11
+  return request({
12
+    'url': '/login',
13
+    headers: {
14
+      isToken: false
15
+    },
16
+    'method': 'post',
17
+    'data': data
18
+  })
19
+}
20
+
21
+// 注册方法
22
+export function register(data) {
23
+  return request({
24
+    url: '/register',
25
+    headers: {
26
+      isToken: false
27
+    },
28
+    method: 'post',
29
+    data: data
30
+  })
31
+}
32
+
33
+// 获取用户详细信息
34
+export function getInfo() {
35
+  return request({
36
+    'url': '/getInfo',
37
+    'method': 'get'
38
+  })
39
+}
40
+
41
+// 退出方法
42
+export function logout() {
43
+  return request({
44
+    'url': '/logout',
45
+    'method': 'post'
46
+  })
47
+}
48
+
49
+// 获取验证码
50
+export function getCodeImg() {
51
+  return request({
52
+    'url': '/captchaImage',
53
+    headers: {
54
+      isToken: false
55
+    },
56
+    method: 'get',
57
+    timeout: 20000
58
+  })
59
+}

+ 35 - 0
src/api/myToDoList/myToDoList.js

@@ -0,0 +1,35 @@
1
+import request from '@/utils/request'
2
+
3
+// 获取当前用户的抄送列表详情
4
+export function listCheckApprovalCcDetails(query) {
5
+  return request({
6
+    url: '/system/check/approval/cc/details',
7
+    method: 'get',
8
+    params: query
9
+  })
10
+}
11
+// 获取待办任务列表
12
+export function listCheckPendingTasks(query) {
13
+  return request({
14
+    url: '/system/check/approval/tasks/pending',
15
+    method: 'get',
16
+    params: query
17
+  })
18
+}
19
+
20
+//获取已办任务列表
21
+export function listCheckFinishedTasks(query) {
22
+  return request({
23
+    url: '/system/check/approval/tasks/completed',
24
+    method: 'get',
25
+    params: query
26
+  })
27
+}
28
+//批量更新抄送消息已读状态
29
+export function updateCheckApprovalCcDetails(data) {
30
+  return request({
31
+    url: '/system/check/approval/cc/batch-read',
32
+    method: 'put',
33
+    data
34
+  })
35
+}

+ 28 - 0
src/api/problemRect/problemRect.js

@@ -0,0 +1,28 @@
1
+import request from '@/utils/request'
2
+//获取问题整改详细信息
3
+export const getProblemRectDetail = (id) => {
4
+    return request({
5
+        url: `/check/checkCorrection/${id}`,
6
+        method: 'get',
7
+    })
8
+}
9
+
10
+//审批同意
11
+export const approvalAgree = (params) => {
12
+    return request({
13
+        url: '/check/checkCorrection/approveTask',
14
+        method: 'post',
15
+        data: params
16
+    })
17
+}
18
+
19
+//审批拒绝
20
+export const approvalReject = (params) => {
21
+    return request({
22
+        url: '/check/checkCorrection/rejectTask',
23
+        method: 'post',
24
+        data: params
25
+    })
26
+}
27
+
28
+

+ 212 - 0
src/api/qualityControlAnalysisReport/qualityControlAnalysisReport.js

@@ -0,0 +1,212 @@
1
+import request from '@/utils/request'
2
+
3
+// 质控活动
4
+// 勤务组织相关接口
5
+export function getAnalysisReport(data) {
6
+  return request({
7
+    url: '/system/analysisReport/check',
8
+    method: 'post',
9
+    data: data
10
+  })
11
+}
12
+
13
+// 出勤人次分析
14
+export function getCalculate(data) {
15
+  return request({
16
+    url: '/quality/attendance/calculate',
17
+    method: 'post',
18
+    data: data
19
+  })
20
+}
21
+
22
+// 出勤人次趋势数据
23
+export function getCalculateTrendData(data) {
24
+  return request({
25
+    url: '/quality/attendance/trend-data',
26
+    method: 'post',
27
+    data: data
28
+  })
29
+}
30
+
31
+// 获取资质等级分布饼状图数据
32
+export function getQualificationPieChart(query) {
33
+  return request({
34
+    url: '/statistics/qualification/pie-chart',
35
+    method: 'get',
36
+    params: query
37
+  })
38
+}
39
+
40
+// 获取资质等级分布柱状图数据
41
+export function getQualificationBarChart(query) {
42
+  return request({
43
+    url: '/statistics/qualification/bar-chart',
44
+    method: 'get',
45
+    params: query
46
+  })
47
+}
48
+
49
+// 质控活动相关接口
50
+// 获取物品分类统计
51
+export function getCategoryStats(query) {
52
+  return request({
53
+    url: '/quality/item-category-stats/category-stats',
54
+    method: 'get',
55
+    params: query
56
+  })
57
+}
58
+
59
+// 获取查获时段趋势图
60
+export function getSeizureTimeTrend(query) {
61
+  return request({
62
+    url: '/quality/item-category-stats/seizure-time-trend',
63
+    method: 'get',
64
+    params: query
65
+  })
66
+}
67
+
68
+// 获取隐匿夹带部位分布统计
69
+export function getConcealmentPositionStats(query) {
70
+  return request({
71
+    url: '/quality/item-category-stats/concealment-position-stats',
72
+    method: 'get',
73
+    params: query
74
+  })
75
+}
76
+
77
+// 获取岗位分类统计
78
+export function getPostCategoryStats(query) {
79
+  return request({
80
+    url: '/quality/item-category-stats/post-category-stats',
81
+    method: 'get',
82
+    params: query
83
+  })
84
+}
85
+
86
+// 获取通道排名统计
87
+export function getChannelRankingStats(query) {
88
+  return request({
89
+    url: '/quality/item-category-stats/channel-ranking-stats',
90
+    method: 'get',
91
+    params: query
92
+  })
93
+}
94
+
95
+// 获取各科室查获排名
96
+export function getDepartmentRanking(query) {
97
+  return request({
98
+    url: '/quality/item-category-stats/brigade-ranking',
99
+    method: 'get',
100
+    params: query
101
+  })
102
+}
103
+
104
+// 风险隐患相关接口
105
+// 移交公安数据
106
+export function getPoliceData(params = {}) {
107
+  return request({
108
+    url: '/quality/item-category-stats/police-data',
109
+    method: 'get',
110
+    params
111
+  })
112
+}
113
+
114
+// 移交公安数据统计
115
+export function getPoliceDataStats(params = {}) {
116
+  return request({
117
+    url: '/quality/item-category-stats/police-stats',
118
+    method: 'get',
119
+    params
120
+  })
121
+}
122
+
123
+// X光机漏检数据
124
+export function getXrayMissCheck(params = {}) {
125
+  return request({
126
+    url: '/quality/item-category-stats/xray-miss-check',
127
+    method: 'get',
128
+    params
129
+  })
130
+}
131
+
132
+// X光机漏检人员统计TOP3
133
+export function getXrayMissCheckStats(params = {}) {
134
+  return request({
135
+    url: '/quality/item-category-stats/xray-miss-check-top3',
136
+    method: 'get',
137
+    params
138
+  })
139
+}
140
+
141
+// 异常查获数据
142
+export function getAbnormalSeizureData(params = {}) {
143
+  return request({
144
+    url: '/quality/item-category-stats/abnormal-seizure-data',
145
+    method: 'get',
146
+    params
147
+  })
148
+}
149
+
150
+// 异常查获数据TOP3
151
+export function getAbnormalSeizureStats(params = {}) {
152
+  return request({
153
+    url: '/quality/item-category-stats/abnormal-seizure-data-top3',
154
+    method: 'get',
155
+    params
156
+  })
157
+}
158
+
159
+// 抽问抽答相关接口
160
+// 抽问抽答完成趋势
161
+export function getCompletionTrend(params = {}) {
162
+  return request({
163
+    url: '/v1/cs/app/daily-exam/completion-comparison',
164
+    method: 'get',
165
+    params
166
+  })
167
+}
168
+
169
+// 错题分析 - 总体问题分布
170
+export function getWrongAnalysisOverview(params = {}) {
171
+  return request({
172
+    url: '/v1/cs/app/daily-exam/wrong-analysis/pc-overview',
173
+    method: 'get',
174
+    params
175
+  })
176
+}
177
+
178
+// 错题分析 - 问题分布对比(雷达图)
179
+export function getWrongAnalysisRadar(params = {}) {
180
+  return request({
181
+    url: '/v1/cs/app/daily-exam/wrong-analysis/pc-radar',
182
+    method: 'get',
183
+    params
184
+  })
185
+}
186
+
187
+// 基于时间维度的绩效统计查询趋势图
188
+export function getCalculateByTime(data) {
189
+  return request({
190
+    url: '/performance/dimension/calculate-by-time',
191
+    method: 'post',
192
+    data: data
193
+  })
194
+}
195
+
196
+// 基于时间维度的绩效统计查询列表
197
+export function getCalculateByTimeList(data) {
198
+  return request({
199
+    url: '/performance/dimension/calculate-by-time-list',
200
+    method: 'post',
201
+    data: data
202
+  })
203
+}
204
+
205
+// 使用报告
206
+export function getUsageReport(query) {
207
+  return request({
208
+    url: '/system/usageReport/report',
209
+    method: 'get',
210
+    params: query
211
+  })
212
+}

+ 46 - 0
src/api/questionStatistics/questionStatistics.js

@@ -0,0 +1,46 @@
1
+import request from '@/utils/request'
2
+
3
+//获取当天所有用户统计数据
4
+export function getDailyAllUsersRanking(params = {}) {
5
+  return request({
6
+    url: '/exam/daily/statistics/dashboard/all-users-ranking',
7
+    method: 'get',
8
+    params
9
+  })
10
+}
11
+
12
+// 抽问抽答完成趋势
13
+export function getCompletionTrend(params = {}) {
14
+  return request({
15
+    url: '/v1/cs/app/daily-exam/completion-trend',
16
+    method: 'get',
17
+    params
18
+  })
19
+}
20
+
21
+// 错题分析 - 总体问题分布
22
+export function getWrongAnalysisOverview(params = {}) {
23
+  return request({
24
+    url: '/v1/cs/app/daily-exam/wrong-analysis/overview',
25
+    method: 'get',
26
+    params
27
+  })
28
+}
29
+
30
+// 错题分析 - 问题分布对比(雷达图)
31
+export function getWrongAnalysisRadar(params = {}) {
32
+  return request({
33
+    url: '/v1/cs/app/daily-exam/wrong-analysis/radar',
34
+    method: 'get',
35
+    params
36
+  })
37
+}
38
+
39
+// 错题分析 - 大类详情
40
+export function getWrongAnalysisCategoryDetail(params = {}) {
41
+  return request({
42
+    url: '/v1/cs/app/daily-exam/wrong-analysis/category-detail',
43
+    method: 'get',
44
+    params
45
+  })
46
+}

+ 54 - 0
src/api/seizeStatistics/seizeStatistics.js

@@ -0,0 +1,54 @@
1
+import request from '@/utils/request'
2
+//查获总数量+移交公安数量+故意隐匿数量
3
+export function getTotalSome(params = {}) {
4
+  return request({
5
+    url: '/item/largeScreen/getAppTotalSome',
6
+    method: 'get',
7
+    params
8
+  })
9
+}
10
+
11
+
12
+//查获岗位分布
13
+export function getPost(params = {}) {
14
+  return request({
15
+    url: '/item/largeScreen/post',
16
+    method: 'get',
17
+    params
18
+  })
19
+}
20
+
21
+//查获位置分布
22
+export function getPosition(params = {}) {
23
+  return request({
24
+    url: '/item/largeScreen/appPosition',
25
+    method: 'get',
26
+    params
27
+  })
28
+}
29
+
30
+//查获排名
31
+export function getRank(params = {}) {
32
+  return request({
33
+    url: '/item/largeScreen/rank',
34
+    method: 'get',
35
+    params
36
+  })
37
+}
38
+//查获时段分布
39
+export function getTimeSpan(params = {}) {
40
+  return request({
41
+    url: '/item/largeScreen/appTimeSpan',
42
+    method: 'get',
43
+    params
44
+  })
45
+}
46
+
47
+//查获类别分布
48
+export function getCategory(params = {}) {
49
+  return request({
50
+    url: '/item/largeScreen/category',
51
+    method: 'get',
52
+    params
53
+  })
54
+}

+ 38 - 0
src/api/seizure/seizureRecord.js

@@ -0,0 +1,38 @@
1
+import request from '@/utils/request'
2
+
3
+// 获取查获接口列表
4
+export function getSeizureRecordList(params = {}) {
5
+  return request({
6
+    url: '/item/record/list',
7
+    method: 'get',
8
+    params
9
+  });
10
+}
11
+
12
+
13
+// 获取查获接口列表
14
+export function addSeizureRecord(data) {
15
+  return request({
16
+    url: '/item/record/add',
17
+    method: 'post',
18
+    data: data
19
+  })
20
+}
21
+
22
+//根据id查询上报的详细信息
23
+export function getInfo(id) {
24
+  return request({
25
+    url: `/item/record/${id}`,   //  反引号
26
+    method: 'get'
27
+  })
28
+}
29
+
30
+
31
+//提交上报
32
+export function approvalStart (params) {
33
+  return request({
34
+    url: '/system/approval/start/seizure',
35
+    method: 'post',
36
+    data: params
37
+  });
38
+}

+ 64 - 0
src/api/seizureRecord/seizureRecord.js

@@ -0,0 +1,64 @@
1
+import request from '@/utils/request'
2
+
3
+// 根据筛选条件查询分类数量
4
+// export function selectGroupCount(params = {}) {
5
+//   return request({
6
+//     url: '/item/items/selectGroupCount',
7
+//     method: 'get',
8
+//     params
9
+//   });
10
+// }
11
+
12
+
13
+// 根据筛选条件查询查获物品明细列表
14
+export function selectByConditions(params = {}) {
15
+  return request({
16
+    url: '/item/items/selectByConditions',
17
+     method: 'get',
18
+    params
19
+  })
20
+}
21
+
22
+// 根据筛选条件查询分类数量
23
+export function selectGroupCount(params = {}) {
24
+  return request({
25
+    url: '/item/items/selectCount',
26
+    method: 'get',
27
+    params
28
+  });
29
+}
30
+
31
+/**
32
+ * @name 根据时段获取当前用户的上岗位置信息
33
+ * @param {*} params.searchtime YYYY-MM-DD hh:mm:ss 
34
+*/
35
+export function getLocationsbyTime (params) {
36
+  return request({
37
+    url: '/attendance/postRecord/getLocationsbyTime',
38
+    method: 'post',
39
+    data: params
40
+  });
41
+}
42
+
43
+/**
44
+ * @name 获取分类信息(包含父类)
45
+ * @param {*} type (1表示常用违禁品, 2表示常用查获部位)
46
+*/
47
+export function categoryInfo (type) {
48
+  return request({
49
+    url: `/system/defaultChoise/categoryInfo/${type}`,
50
+    method: 'get'
51
+  });
52
+}
53
+
54
+/**
55
+ * @name 对违禁品类进行模糊搜索
56
+ * @param {*} name 查询字符串
57
+*/
58
+export function categoryList (params) {
59
+  return request({
60
+    url: `/system/category/list`,
61
+    method: 'get',
62
+    params
63
+  });
64
+}

+ 37 - 0
src/api/statisticalAnalysis/statisticalAnalysis.js

@@ -0,0 +1,37 @@
1
+import request from '@/utils/request'
2
+
3
+// 获取绩效列表
4
+export function getItemPerformanceList(params) {
5
+  return request({
6
+    url: '/item/performance/list',
7
+    method: 'get',
8
+    params
9
+  })
10
+}
11
+
12
+// 获取绩效柱状图数据
13
+export function getItemPerformanceHistogram(params) {
14
+  return request({
15
+    url: '/item/performance/histogram',
16
+    method: 'get',
17
+    params
18
+  })
19
+}
20
+
21
+// 获取绩效箱线图数据
22
+export function getItemPerformanceBoxplot(params) {
23
+  return request({
24
+    url: '/item/performance/boxplot',
25
+    method: 'get',
26
+    params
27
+  })
28
+}
29
+
30
+// 获取绩效指标数据
31
+export function getItemPerformanceMetrics(data) {
32
+  return request({
33
+    url: '/item/performance/metrics',
34
+    method: 'post',
35
+    data
36
+  })
37
+}

+ 14 - 0
src/api/system/common.js

@@ -0,0 +1,14 @@
1
+import request from '@/utils/request'
2
+
3
+/*
4
+ * 查询分类下拉树结构
5
+ * @param {*} treeType ITEM_CATEGORY  查获物品分类   POSITION  位置   CHECK_POINT 检查部位  CHECK_CATEGORY 检查分类
6
+ * @returns 
7
+ */
8
+export function treeSelectByType(treeType,maxLevel,lable) {
9
+    return request({
10
+      url: '/dataConfig/dataConfigTree',
11
+      method: 'get',
12
+      params: {treeType,lable,maxLevel}
13
+    })
14
+  }

+ 31 - 0
src/api/system/dept/dept.js

@@ -0,0 +1,31 @@
1
+import request from '@/utils/request'
2
+
3
+// 获取组织树形结构
4
+export function getDeptList(params) {
5
+  return request({
6
+    url: '/system/user/deptTree',
7
+    // headers: {
8
+    //   isToken: false
9
+    // },
10
+    method: 'get',
11
+    params: params
12
+    // params: {deptType: "TEAMS"}
13
+  })
14
+}
15
+
16
+
17
+//查询部门负责人
18
+export function getDeptManager(deptId) {
19
+  return request({
20
+    url: `/system/dept/deptLeader/${deptId}`,
21
+    method: 'get',
22
+  })
23
+}
24
+
25
+//获取部门详细信息
26
+export function getDeptDetail(deptId) {
27
+  return request({
28
+    url: `/system/dept/${deptId}`,
29
+    method: 'get',
30
+  })
31
+}

+ 52 - 0
src/api/system/dict/data.js

@@ -0,0 +1,52 @@
1
+import request from '@/utils/request'
2
+
3
+// 查询字典数据列表
4
+export function listData(query) {
5
+  return request({
6
+    url: '/system/dict/data/list',
7
+    method: 'get',
8
+    params: query
9
+  })
10
+}
11
+
12
+// 查询字典数据详细
13
+export function getData(dictCode) {
14
+  return request({
15
+    url: '/system/dict/data/' + dictCode,
16
+    method: 'get'
17
+  })
18
+}
19
+
20
+// 根据字典类型查询字典数据信息
21
+export function getDicts(dictType) {
22
+  return request({
23
+    url: '/system/dict/data/type/' + dictType,
24
+    method: 'get'
25
+  })
26
+}
27
+
28
+// 新增字典数据
29
+export function addData(data) {
30
+  return request({
31
+    url: '/system/dict/data',
32
+    method: 'post',
33
+    data: data
34
+  })
35
+}
36
+
37
+// 修改字典数据
38
+export function updateData(data) {
39
+  return request({
40
+    url: '/system/dict/data',
41
+    method: 'put',
42
+    data: data
43
+  })
44
+}
45
+
46
+// 删除字典数据
47
+export function delData(dictCode) {
48
+  return request({
49
+    url: '/system/dict/data/' + dictCode,
50
+    method: 'delete'
51
+  })
52
+}

+ 60 - 0
src/api/system/dict/type.js

@@ -0,0 +1,60 @@
1
+import request from '@/utils/request'
2
+
3
+// 查询字典类型列表
4
+export function listType(query) {
5
+  return request({
6
+    url: '/system/dict/type/list',
7
+    method: 'get',
8
+    params: query
9
+  })
10
+}
11
+
12
+// 查询字典类型详细
13
+export function getType(dictId) {
14
+  return request({
15
+    url: '/system/dict/type/' + dictId,
16
+    method: 'get'
17
+  })
18
+}
19
+
20
+// 新增字典类型
21
+export function addType(data) {
22
+  return request({
23
+    url: '/system/dict/type',
24
+    method: 'post',
25
+    data: data
26
+  })
27
+}
28
+
29
+// 修改字典类型
30
+export function updateType(data) {
31
+  return request({
32
+    url: '/system/dict/type',
33
+    method: 'put',
34
+    data: data
35
+  })
36
+}
37
+
38
+// 删除字典类型
39
+export function delType(dictId) {
40
+  return request({
41
+    url: '/system/dict/type/' + dictId,
42
+    method: 'delete'
43
+  })
44
+}
45
+
46
+// 刷新字典缓存
47
+export function refreshCache() {
48
+  return request({
49
+    url: '/system/dict/type/refreshCache',
50
+    method: 'delete'
51
+  })
52
+}
53
+
54
+// 获取字典选择框列表
55
+export function optionselect() {
56
+  return request({
57
+    url: '/system/dict/type/optionselect',
58
+    method: 'get'
59
+  })
60
+}

+ 128 - 0
src/api/system/user.js

@@ -0,0 +1,128 @@
1
+import upload from '@/utils/upload'
2
+import request from '@/utils/request'
3
+
4
+// 用户密码重置
5
+export function updateUserPwd(oldPassword, newPassword) {
6
+  const data = {
7
+    oldPassword,
8
+    newPassword
9
+  }
10
+  return request({
11
+    url: '/system/user/profile/updatePwd',
12
+    method: 'put',
13
+    data: data
14
+  })
15
+}
16
+
17
+// 查询用户个人信息
18
+export function getUserProfile() {
19
+  return request({
20
+    url: '/system/user/profile',
21
+    method: 'get'
22
+  })
23
+}
24
+
25
+// 修改用户个人信息
26
+export function updateUserProfile(data) {
27
+  return request({
28
+    url: '/system/user/profile',
29
+    method: 'put',
30
+    data: data
31
+  })
32
+}
33
+
34
+// 用户头像上传
35
+export function uploadAvatar(data) {
36
+  return upload({
37
+    url: '/system/user/profile/avatar',
38
+    name: data.name,
39
+    filePath: data.filePath
40
+  })
41
+}
42
+
43
+// 查询用户列表
44
+export function getUserList() {
45
+  return request({
46
+    url: '/system/user/list',
47
+    method: 'get',
48
+    params: {
49
+      pageNum: 1,
50
+      pageSize: 500
51
+    }
52
+  })
53
+}
54
+
55
+// 查询部门下拉树结构
56
+export function deptTreeSelect() {
57
+  return request({
58
+    url: '/system/user/deptTree',
59
+    method: 'get'
60
+  })
61
+}
62
+
63
+//获取全部用户
64
+export function listAllUser() {
65
+  return request({
66
+    url: '/system/user/listAll',
67
+    method: 'get',
68
+  })
69
+}
70
+
71
+//根据用户ID查询用户信息
72
+export function getUserInfoById(userId) {
73
+  return request({
74
+    url: `/system/user/${userId}`,
75
+    method: 'get',
76
+  })
77
+}
78
+
79
+//根据用户ID获取岗位选择框列表
80
+export function getPostListsByUserId(userId) {
81
+  return request({
82
+    url: `/system/user/getPostListsByUserId/${userId}`,
83
+    method: 'get',
84
+  })
85
+}
86
+
87
+//获取所有部门和班组下人员
88
+export function getDeptUserTree(params) {
89
+  return request({
90
+    url: '/system/user/deptUserTree',
91
+    method: 'get',
92
+    params: params
93
+  })
94
+}
95
+
96
+//根据角色ID查询应用列表
97
+export function getAppListByRoleId(roleId) {
98
+  return request({
99
+    url: `/system/app/roleAppSelect/${roleId}`, 
100
+    method: 'get',
101
+  })
102
+}
103
+
104
+//查询移动端全部应用列表
105
+export function getAppList(params) {
106
+  return request({
107
+    url: '/system/app/listBy',
108
+    method: 'get',
109
+    params: params
110
+  })
111
+}
112
+
113
+// 修改用户
114
+export function updateUser(data) {
115
+  return request({
116
+    url: '/system/user',
117
+    method: 'put',
118
+    data: data
119
+  })
120
+}
121
+
122
+//查询所有岗位
123
+export function getPostAllList() {
124
+  return request({
125
+    url: '/system/post/listAllTree',
126
+    method: 'get',
127
+  })
128
+}

+ 24 - 0
src/api/voiceSubmissionDraft/voiceSubmissionDraft.js

@@ -0,0 +1,24 @@
1
+import request from '@/utils/request'
2
+
3
+// 查获草稿列表的接口
4
+export function listVoiceSubmissionDraft(query) {
5
+    return request({
6
+        url: '/item/failedMatch/myList',
7
+        method: 'get',
8
+        params: query
9
+    })
10
+}
11
+//查询详情
12
+export function getVoiceInfo(voiceId) {
13
+    return request({
14
+        url: `/item/failedMatch/${voiceId}`,
15
+        method: 'get',
16
+    })
17
+}
18
+//删除草稿
19
+export function deleteVoiceDraft(voiceId) {
20
+    return request({
21
+        url: `/item/failedMatch/${voiceId}`,
22
+        method: 'delete',
23
+    })
24
+}

+ 18 - 0
src/api/workDocu/workDocu.js

@@ -0,0 +1,18 @@
1
+import request from '@/utils/request'
2
+
3
+// 查询工作文档列表
4
+export function listWorkDocu(query) {
5
+    return request({
6
+        url: '/system/workingDocument/list',
7
+        method: 'get',
8
+        params: query
9
+    })
10
+}
11
+
12
+//获取工作文档详细信息
13
+export function getWorkDocu(id) {
14
+    return request({
15
+        url: '/system/workingDocument/' + id,
16
+        method: 'get'
17
+    })
18
+}

+ 83 - 0
src/api/workProfile/workProfile.js

@@ -0,0 +1,83 @@
1
+import request from '@/utils/request'
2
+//获取站级别抽问抽答完成率
3
+export function getStationLevelRate (params) {
4
+  return request({
5
+    url: '/exam/daily/site-profile/daily-completion-rate',
6
+    method: 'get',
7
+    data: params
8
+  });
9
+}
10
+//获取部门抽问抽答完成率
11
+export function getDepartmentLevelRate (params) {
12
+  return request({
13
+    url: '/exam/daily/dept-profile/daily-completion-rate',
14
+    method: 'get',
15
+    data: params
16
+  });
17
+}
18
+//工作画像-总体概览
19
+export function getWorkingPortrait (params) {
20
+  return request({
21
+    url: '/system/user/workingPortrait',
22
+    method: 'get',
23
+    data: params
24
+  });
25
+}
26
+//工作画像-组织支撑-培训测试平均分趋势图
27
+export function getTrainingTestScoreTrend (params) {
28
+  return request({
29
+    url: '/system/growth/organizationalSupport',
30
+    method: 'get',
31
+    data: params
32
+  });
33
+}
34
+//工作画像-管理推动-检查单
35
+export function getManagementRecord (params) {
36
+  return request({
37
+    url: '/check/largeScreen/managementPromotionRecord',
38
+    method: 'get',
39
+    data: params
40
+  });
41
+}
42
+//工作画像-管理推动-整改单
43
+export function getManagementRectify (params) {
44
+  return request({
45
+    url: '/check/largeScreen/managementPromotionCorrection',
46
+    method: 'get',
47
+    data: params
48
+  });
49
+}
50
+//工作画像-工作产出-巡检问题统计图
51
+export function getInspectionProblemChart (params) {
52
+  return request({
53
+    url: '/check/largeScreen/workOutputCheck',
54
+    method: 'get',
55
+    data: params
56
+  });
57
+}
58
+//工作画像--通道开放趋势图(折线图)
59
+export function getChannelOpenTrendChart (params) {
60
+  return request({
61
+    url: '/attendance/stats/channel/open/trend',
62
+    method: 'get',
63
+    data: params
64
+  });
65
+}
66
+//工作画像--查获审批时长统计(柱状图)
67
+export function getDurationChart (params) {
68
+  return request({
69
+    url: '/item/user-ranking/seizure-approval/duration',
70
+    method: 'get',
71
+    data: params
72
+  });
73
+}
74
+//工作画像--查获趋势图,获取有效查获趋势数据(默认近90天)
75
+export function getSeizureTrendChart (params) {
76
+  return request({
77
+    url: '/item/user-ranking/seizure-trend',
78
+    method: 'get',
79
+    data: params
80
+  });
81
+}
82
+
83
+

+ 39 - 0
src/components/HeadTitle.vue

@@ -0,0 +1,39 @@
1
+<template>
2
+  <div class="head-title">
3
+    {{ title }}
4
+    <div v-if="subTitle" class="sub-title">{{ subTitle }}</div>
5
+  </div>
6
+</template>
7
+
8
+<script>
9
+export default {
10
+  props: {
11
+    title: {
12
+      type: String,
13
+      default: ''
14
+    },
15
+    subTitle: {
16
+      type: String,
17
+      default: ''
18
+    }
19
+  }
20
+}
21
+</script>
22
+
23
+<style lang="scss" scoped>
24
+.head-title {
25
+  padding-top: 20rpx;
26
+  font-weight: bold;
27
+  font-size: 32rpx;
28
+  color: #3D3D3D;
29
+  line-height: 44rpx;
30
+
31
+  .sub-title {
32
+    margin: 8rpx 0 4rpx;
33
+    font-weight: 400;
34
+    font-size: 24rpx;
35
+    color: #999999;
36
+    line-height: 28rpx;
37
+  }
38
+}
39
+</style>

+ 96 - 0
src/components/HomeContainer.vue

@@ -0,0 +1,96 @@
1
+<template>
2
+  <div class="home-container" :style="customHomeStyle" ref="containerRef">
3
+    <div class="container" :style="customStyle">
4
+      <slot></slot>
5
+    </div>
6
+  </div>
7
+</template>
8
+
9
+<script>
10
+export default {
11
+  props: {
12
+    customHomeStyle: {
13
+      type: Object,
14
+      default: () => ({})
15
+    },
16
+    customStyle: {
17
+      type: Object,
18
+      default: () => ({})
19
+    }
20
+  },
21
+  data() {
22
+    return {
23
+      scrollTimer: null
24
+    }
25
+  },
26
+  mounted() {
27
+    this.$nextTick(() => {
28
+      this.addScrollListener()
29
+    })
30
+  },
31
+  beforeDestroy() {
32
+    this.removeScrollListener()
33
+  },
34
+  methods: {
35
+    addScrollListener() {
36
+      const container = this.$refs.containerRef
37
+      if (container) {
38
+        container.addEventListener('scroll', this.handleScroll)
39
+      }
40
+    },
41
+    removeScrollListener() {
42
+      const container = this.$refs.containerRef
43
+      if (container) {
44
+        container.removeEventListener('scroll', this.handleScroll)
45
+      }
46
+      if (this.scrollTimer) {
47
+        clearTimeout(this.scrollTimer)
48
+      }
49
+    },
50
+    handleScroll() {
51
+      // 触发滚动事件
52
+      this.$emit('scroll')
53
+      
54
+      // 清除之前的定时器
55
+      if (this.scrollTimer) {
56
+        clearTimeout(this.scrollTimer)
57
+      }
58
+      
59
+      // 设置新的定时器,停止滚动后触发停止事件
60
+      this.scrollTimer = setTimeout(() => {
61
+        this.$emit('scroll-end')
62
+      }, 200)
63
+    },
64
+    
65
+    // 滚动到顶部方法
66
+    scrollToTop() {
67
+      const container = this.$refs.containerRef
68
+      if (container) {
69
+        container.scrollTo({
70
+          top: 0,
71
+          behavior: 'smooth'
72
+        })
73
+      }
74
+    }
75
+  }
76
+}
77
+</script>
78
+
79
+<style lang="scss" scoped>
80
+.home-container {
81
+  height: 100%;
82
+  overflow: auto;
83
+  background: #FFFFFF;
84
+
85
+  .container {
86
+    width: 100%;
87
+    padding: 20rpx;
88
+    min-height: calc(100vh - 80rpx);
89
+    overflow: auto;
90
+    box-sizing: border-box;
91
+    background: url("../static/images/home-bg.png") no-repeat;
92
+    background-size: 100% auto;
93
+    
94
+  }
95
+}
96
+</style>

+ 52 - 0
src/components/UserInfo.vue

@@ -0,0 +1,52 @@
1
+<template>
2
+  <div>
3
+    <div class="user-info">
4
+      亲爱的,{{ name }}
5
+    </div>
6
+
7
+    <div v-if="subTitle || $scopedSlots.default" class="sub-title">
8
+      <slot>{{ subTitle }}</slot>
9
+    </div>
10
+  </div>
11
+</template>
12
+
13
+<script>
14
+import userIcon from "@/static/images/user-icon.png";
15
+
16
+export default {
17
+  props: {
18
+    name: {
19
+      type: String,
20
+      default: ""
21
+    },
22
+    subTitle: {
23
+      type: String,
24
+      default: ""
25
+    }
26
+  },
27
+  data() {
28
+    return {
29
+      userIcon,
30
+    }
31
+  },
32
+}
33
+</script>
34
+
35
+<style lang="scss" scoped>
36
+.user-info {
37
+  padding: 4rpx 0 4rpx 130rpx;
38
+  font-weight: 400;
39
+  font-size: 48rpx;
40
+  color: #FFFFFF;
41
+  background: url("../static/images/user-icon.png") no-repeat;
42
+  background-size: auto 100%;
43
+}
44
+
45
+.sub-title {
46
+  padding: 28rpx 0;
47
+  font-weight: 400;
48
+  font-size: 28rpx;
49
+  color: rgba(255, 255, 255, 0.7);
50
+  line-height: 40rpx;
51
+}
52
+</style>

+ 149 - 0
src/components/approve-history/approve-history.vue

@@ -0,0 +1,149 @@
1
+<template>
2
+  <view class="approval-history">
3
+    <view v-for="(item, index) in historyList" :key="index" class="approval-item">
4
+      <view class="timeline">
5
+        <view class="circle" :class="{ 'circle-submit': item.action !== 'PENDING' }"></view>
6
+        <view class="line" v-if="index < historyList.length - 1" :class="{ 'line-submit': item.action !== 'PENDING' }">
7
+        </view>
8
+      </view>
9
+      <view class="content">
10
+        <view class="header">
11
+          <text class="name">{{ item.operatorName }}</text>
12
+          <text class="date">{{ item.operationTime }}</text>
13
+        </view>
14
+        <view v-if="item.comment" class="approval-content">{{ item.action === 'REJECT' ? `审批驳回:${item.comment}`
15
+          : `${item.comment}` }}</view>
16
+      </view>
17
+    </view>
18
+
19
+    <!-- 空状态 -->
20
+    <view v-if="historyList.length === 0" class="empty-state">
21
+      <text class="empty-text">暂无审批记录</text>
22
+    </view>
23
+  </view>
24
+</template>
25
+
26
+<script>
27
+export default {
28
+  name: 'ApproveHistory',
29
+  props: {
30
+    // 审批历史数据列表
31
+    historyList: {
32
+      type: Array,
33
+      default: () => []
34
+    },
35
+    // 是否显示空状态
36
+    showEmpty: {
37
+      type: Boolean,
38
+      default: true
39
+    }
40
+  },
41
+  data() {
42
+    return {}
43
+  },
44
+  computed: {
45
+    // 格式化后的历史数据
46
+    formattedHistory() {
47
+      return this.historyList.map(item => ({
48
+        operatorName: item.operatorName || '未知用户',
49
+        operationTime: item.operationTime || '',
50
+        comment: item.comment || '',
51
+        status: item.status || '',
52
+        nodeName: item.nodeName || ''
53
+      }))
54
+    }
55
+  },
56
+  methods: {}
57
+}
58
+</script>
59
+
60
+<style lang="scss" scoped>
61
+.approval-history {
62
+  padding: 15px;
63
+
64
+  .approval-item {
65
+    display: flex;
66
+    margin-bottom: 1rpx;
67
+
68
+    &:last-child {
69
+      margin-bottom: 0;
70
+    }
71
+
72
+    .timeline {
73
+      display: flex;
74
+      flex-direction: column;
75
+      align-items: center;
76
+      margin-right: 12px;
77
+
78
+      .circle {
79
+        width: 12px;
80
+        height: 12px;
81
+        border-radius: 50%;
82
+        background-color: #409EFF;
83
+        border: 2px solid #fff;
84
+        box-shadow: 0 0 0 2px #409EFF;
85
+      }
86
+
87
+      .circle-submit {
88
+        background-color: #909399 !important;
89
+        box-shadow: 0 0 0 2px #909399 !important;
90
+      }
91
+
92
+      .line {
93
+        width: 2px;
94
+        flex: 1;
95
+        background-color: #409EFF;
96
+        margin-top: 4px;
97
+      }
98
+
99
+      .line-submit {
100
+        background-color: #909399 !important;
101
+      }
102
+    }
103
+
104
+    .content {
105
+      flex: 1;
106
+      position: relative;
107
+      bottom: 8rpx;
108
+
109
+      .header {
110
+        display: flex;
111
+        justify-content: space-between;
112
+        align-items: center;
113
+        margin-bottom: 8px;
114
+
115
+        .name {
116
+          font-size: 14px;
117
+          font-weight: 500;
118
+          color: #333;
119
+        }
120
+
121
+        .date {
122
+          font-size: 12px;
123
+          color: #999;
124
+        }
125
+      }
126
+
127
+      .approval-content {
128
+        font-size: 13px;
129
+        color: #666;
130
+        border: 1px solid #DDDDDD;
131
+        line-height: 1.5;
132
+        background-color: #f8f9fa;
133
+        padding: 10px;
134
+        border-radius: 6px;
135
+      }
136
+    }
137
+  }
138
+
139
+  .empty-state {
140
+    text-align: center;
141
+    padding: 40px 0;
142
+
143
+    .empty-text {
144
+      font-size: 14px;
145
+      color: #999;
146
+    }
147
+  }
148
+}
149
+</style>

+ 150 - 0
src/components/custom-tabbar.vue

@@ -0,0 +1,150 @@
1
+<template>
2
+  <view class="custom-tabbar">
3
+    <view v-for="(item, index) in list" :key="index" @click="switchTab(item)" class="tabbar-item">
4
+      <image :src="currentPage === item.pagePath ? item.selectedIconPath : item.iconPath"
5
+        style="width: 24px; height: 24px;" />
6
+      <text style="font-size: 12px; margin-top: 4px;">{{ item.text }}</text>
7
+    </view>
8
+  </view>
9
+</template>
10
+
11
+<script>
12
+export default {
13
+  data() {
14
+    return {
15
+      list: [
16
+        {
17
+          pagePath: "pages/home-new/index",
18
+          iconPath: "/static/images/tabbar/home.png",
19
+          selectedIconPath: "/static/images/tabbar/home_.png",
20
+          text: "首页"
21
+        },
22
+        {
23
+          pagePath: "pages/myToDoList/index",
24
+          iconPath: "/static/images/tabbar/message.png",
25
+          selectedIconPath: "/static/images/tabbar/message_.png",
26
+          text: "消息"
27
+        },
28
+        {
29
+          pagePath: "pages/work/index",
30
+          iconPath: "/static/images/tabbar/work.png",
31
+          selectedIconPath: "/static/images/tabbar/work_.png",
32
+          text: "工作台"
33
+        },
34
+        {
35
+          pagePath: "pages/mine/index",
36
+          iconPath: "/static/images/tabbar/mine.png",
37
+          selectedIconPath: "/static/images/tabbar/mine_.png",
38
+          text: "我的"
39
+        }
40
+      ],
41
+      currentPage: ""
42
+    };
43
+  },
44
+  mounted() {
45
+    this.updateCurrentPage();
46
+  },
47
+  methods: {
48
+    switchTab(item) {
49
+      uni.switchTab({
50
+        url: `/${item.pagePath}` // 确保路径以 "/" 开头
51
+      });
52
+    },
53
+    updateCurrentPage() {
54
+      const pages = getCurrentPages();
55
+      if (pages.length > 0) {
56
+        let route = pages[pages.length - 1].route;
57
+        // 统一路径格式(如去掉 "/" 前缀)
58
+        this.currentPage = route.startsWith('/') ? route.substring(1) : route;
59
+      }
60
+    },
61
+    
62
+    // 获取未读消息数量
63
+    async getUnreadMessageCount() {
64
+      try {
65
+        // 这里需要调用实际的获取未读消息数量的接口
66
+        // 示例接口:假设是获取待办列表的接口
67
+        const { listCheckPendingTasks } = require('@/api/myToDoList/myToDoList.js');
68
+        const response = await listCheckPendingTasks({ pageNum: 1, pageSize: 1 });
69
+        
70
+        // 假设接口返回的total字段是未读消息总数
71
+        const unreadCount = response.total || 0;
72
+        
73
+        // 更新消息tab的未读数量
74
+        const messageTab = this.list.find(item => item.text === '消息');
75
+        if (messageTab) {
76
+          messageTab.unreadCount = unreadCount;
77
+        }
78
+      } catch (error) {
79
+        console.error('获取未读消息数量失败:', error);
80
+      }
81
+    }
82
+  }
83
+};
84
+</script>
85
+
86
+<style>
87
+.custom-tabbar {
88
+  position: fixed;
89
+  bottom: 0;
90
+  left: 0;
91
+  right: 0;
92
+  z-index: 9999;
93
+  display: flex;
94
+  justify-content: space-around;
95
+  background: #fff;
96
+  padding: 0;
97
+  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
98
+  transform: translateZ(0);
99
+  -webkit-transform: translateZ(0);
100
+  /* 移除重复的padding设置,改为在特定条件下应用 */
101
+}
102
+
103
+.tabbar-item {
104
+  display: flex;
105
+  flex-direction: column;
106
+  align-items: center;
107
+  padding: 8px 0;
108
+}
109
+
110
+.icon-container {
111
+  position: relative;
112
+  width: 24px;
113
+  height: 24px;
114
+}
115
+
116
+.badge {
117
+  position: absolute;
118
+  top: -4px;
119
+  right: -4px;
120
+  width: 12px;
121
+  height: 12px;
122
+  background-color: #F55B5D;
123
+  border-radius: 50%;
124
+  z-index: 10;
125
+}
126
+
127
+/* 为iOS Safari浏览器添加安全区域支持 */
128
+/* 仅在独立浏览器环境中应用,避免在App嵌套环境中造成双重padding */
129
+@media screen and (max-width: 768px) and (orientation: portrait), 
130
+       screen and (max-width: 1024px) and (orientation: landscape) {
131
+  /* 检测移动设备环境 */
132
+  @supports (padding-bottom: env(safe-area-inset-bottom)) {
133
+    /* 确保只在独立浏览器而非App WebView中应用 */
134
+    .custom-tabbar {
135
+      padding-bottom: env(safe-area-inset-bottom, 0px);
136
+    }
137
+  }
138
+}
139
+
140
+/* 兼容性处理 */
141
+@supports (-webkit-touch-callout: none) and (not (min-device-width: 768px)) {
142
+  /* 针对移动设备的WebView特殊处理 */
143
+  .custom-tabbar {
144
+    /* App内嵌时,使用绝对定位确保贴合底部 */
145
+    position: absolute;
146
+    bottom: 0;
147
+    width: 100%;
148
+  }
149
+}
150
+</style>

+ 284 - 0
src/components/fuzzy-select/fuzzy-select.vue

@@ -0,0 +1,284 @@
1
+<template>
2
+  <view class="fuzzy-select-container">
3
+    <!-- 搜索输入框和删除按钮 -->
4
+    <view class="input-container">
5
+      <uni-easyinput ref="inputRef" v-model="searchKeyword" :placeholder="placeholder" :clearable="true"
6
+        :disabled="disabled" @input="handleInput" @focus="showDropdown = true" @blur="handleBlur" />
7
+      
8
+      <!-- 删除按钮 -->
9
+      <view v-if="value && !disabled" class="delete-btn" @click="handleDelete">
10
+        <text class="delete-icon">×</text>
11
+      </view>
12
+    </view>
13
+
14
+    <!-- 下拉选项列表 -->
15
+    <view v-if="showDropdown && filteredOptions.length > 0" class="dropdown-list">
16
+      <scroll-view scroll-y="true" class="dropdown-scroll">
17
+        <view v-for="(option, index) in filteredOptions" :key="index" class="dropdown-item"
18
+          @click="selectOption(option)">
19
+          {{ option[dataText] }}
20
+        </view>
21
+      </scroll-view>
22
+    </view>
23
+
24
+    <!-- 无匹配结果提示 -->
25
+    <view v-if="showDropdown && filteredOptions.length === 0 && searchKeyword" class="no-result">
26
+      无匹配结果
27
+    </view>
28
+  </view>
29
+</template>
30
+
31
+<script>
32
+export default {
33
+  name: "FuzzySelect",
34
+  props: {
35
+    // 绑定值
36
+    value: {
37
+      type: [String, Number],
38
+      default: ''
39
+    },
40
+    // 选项列表
41
+    options: {
42
+      type: Array,
43
+      default: () => []
44
+    },
45
+    // 占位符
46
+    placeholder: {
47
+      type: String,
48
+      default: '请输入搜索'
49
+    },
50
+    // 值字段名
51
+    dataValue: {
52
+      type: String,
53
+      default: 'value'
54
+    },
55
+    // 显示文本字段名
56
+    dataText: {
57
+      type: String,
58
+      default: 'text'
59
+    },
60
+    // 是否开启搜索
61
+    filterable: {
62
+      type: Boolean,
63
+      default: true
64
+    },
65
+    // 是否禁用
66
+    disabled: {
67
+      type: Boolean,
68
+      default: false
69
+    }
70
+  },
71
+  data() {
72
+    return {
73
+      searchKeyword: '',
74
+      showDropdown: false,
75
+      filteredOptions: this.options,
76
+      selectedOption: null
77
+    }
78
+  },
79
+  watch: {
80
+    options(newOptions) {
81
+      this.filterOptions(this.searchKeyword, newOptions)
82
+      // 当options更新后,需要重新根据当前value设置选中项
83
+      this.updateSelectedOption(this.value)
84
+    },
85
+    value(newValue) {
86
+      // 只有当value确实变化时才更新选中项
87
+      if (newValue !== this.selectedOption?.[this.dataValue]) {
88
+        this.updateSelectedOption(newValue)
89
+      }
90
+    }
91
+  },
92
+  mounted() {
93
+    this.updateSelectedOption(this.value)
94
+  },
95
+  methods: {
96
+    // 根据value值更新选中的选项
97
+    updateSelectedOption(value) {
98
+      
99
+      if (!value) {
100
+        this.searchKeyword = ''
101
+        this.selectedOption = null
102
+        return
103
+      }
104
+
105
+      const foundOption = this.options.find(option =>
106
+        String(option[this.dataValue]) === String(value)
107
+      )
108
+
109
+      if (foundOption) {
110
+        
111
+        this.selectedOption = foundOption
112
+        this.searchKeyword = foundOption[this.dataText]
113
+        // 确保filteredOptions包含当前选中的选项
114
+        if (!this.filteredOptions.some(opt => String(opt[this.dataValue]) === String(value))) {
115
+          this.filteredOptions = this.options
116
+        }
117
+      } else {
118
+        this.searchKeyword = ''
119
+        this.selectedOption = null
120
+      }
121
+    },
122
+    // 处理输入
123
+    handleInput(value) {
124
+      if (this.disabled) return;
125
+      this.searchKeyword = value
126
+      this.filterOptions(value)
127
+      this.showDropdown = true
128
+    },
129
+
130
+    // 过滤选项
131
+    filterOptions(keyword = '', options = this.options) {
132
+      if (!this.filterable || !keyword.trim()) {
133
+        this.filteredOptions = options
134
+        return
135
+      }
136
+
137
+      const lowerKeyword = keyword.toLowerCase()
138
+      this.filteredOptions = options.filter(option => {
139
+        const text = String(option[this.dataText] || '').toLowerCase()
140
+        const value = String(option[this.dataValue] || '').toLowerCase()
141
+        return text.includes(lowerKeyword) || value.includes(lowerKeyword)
142
+      })
143
+    },
144
+
145
+    // 选择选项
146
+    selectOption(option) {
147
+      if (this.disabled) return;
148
+      this.selectedOption = option
149
+      this.searchKeyword = option[this.dataText]
150
+      this.$emit('input', option[this.dataValue])
151
+      this.$emit('change', option[this.dataValue])
152
+      this.showDropdown = false
153
+    },
154
+
155
+    // 处理失焦
156
+    handleBlur() {
157
+      // 延迟隐藏下拉框,确保点击选项能触发
158
+      setTimeout(() => {
159
+        this.showDropdown = false
160
+      }, 200)
161
+    },
162
+
163
+    // 清空搜索
164
+    clearSearch() {
165
+      this.searchKeyword = ''
166
+      this.filteredOptions = this.options
167
+      this.selectedOption = null
168
+      this.$emit('input', '')
169
+      this.$emit('change', '')
170
+    },
171
+
172
+    // 聚焦输入框
173
+    focus() {
174
+      if (this.$refs.inputRef) {
175
+        this.$refs.inputRef.focus()
176
+      }
177
+    },
178
+
179
+    // 失焦输入框
180
+    blur() {
181
+      if (this.$refs.inputRef) {
182
+        this.$refs.inputRef.blur()
183
+      }
184
+    },
185
+
186
+    // 处理删除
187
+    handleDelete() {
188
+      if (this.disabled) return;
189
+      this.clearSearch()
190
+      this.$emit('delete')
191
+    },
192
+  }
193
+}
194
+</script>
195
+
196
+<style scoped>
197
+.fuzzy-select-container {
198
+  position: relative;
199
+  width: 100%;
200
+}
201
+
202
+.input-container {
203
+  position: relative;
204
+  display: flex;
205
+  align-items: center;
206
+}
207
+
208
+.delete-btn {
209
+  position: absolute;
210
+  right: 8px;
211
+  top: 50%;
212
+  transform: translateY(-50%);
213
+  width: 20px;
214
+  height: 20px;
215
+  border-radius: 50%;
216
+  background-color: #f0f0f0;
217
+  display: flex;
218
+  align-items: center;
219
+  justify-content: center;
220
+  cursor: pointer;
221
+  z-index: 10;
222
+}
223
+
224
+.delete-btn:hover {
225
+  background-color: #e0e0e0;
226
+}
227
+
228
+.delete-icon {
229
+  font-size: 16px;
230
+  color: #999;
231
+  font-weight: bold;
232
+  line-height: 1;
233
+}
234
+
235
+.dropdown-list {
236
+  position: absolute;
237
+  top: 100%;
238
+  left: 0;
239
+  right: 0;
240
+  max-height: 200px;
241
+  background-color: #fff;
242
+  border: 1px solid #e5e5e5;
243
+  border-radius: 4px;
244
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
245
+  z-index: 999;
246
+  margin-top: 4px;
247
+}
248
+
249
+.dropdown-scroll {
250
+  max-height: 200px;
251
+}
252
+
253
+.dropdown-item {
254
+  padding: 12px 16px;
255
+  font-size: 14px;
256
+  color: #333;
257
+  border-bottom: 1px solid #f0f0f0;
258
+  cursor: pointer;
259
+}
260
+
261
+.dropdown-item:hover {
262
+  background-color: #f5f5f5;
263
+}
264
+
265
+.dropdown-item:last-child {
266
+  border-bottom: none;
267
+}
268
+
269
+.no-result {
270
+  position: absolute;
271
+  top: 100%;
272
+  left: 0;
273
+  right: 0;
274
+  padding: 12px 16px;
275
+  background-color: #fff;
276
+  border: 1px solid #e5e5e5;
277
+  border-radius: 4px;
278
+  font-size: 14px;
279
+  color: #999;
280
+  text-align: center;
281
+  z-index: 999;
282
+  margin-top: 4px;
283
+}
284
+</style>

+ 138 - 0
src/components/h-collapse-item/h-collapse-item.vue

@@ -0,0 +1,138 @@
1
+<template>
2
+    <uni-collapse-item :name="name" :show-animation="showAnimation" :disabled="disabled" ref="collapseItem" @change="handleCollapseChange">
3
+        <template #title>
4
+            <view class="h-collapse-item-title">
5
+                <view v-if="iconUrl" class="title-icon"
6
+                    :style="{ backgroundImage: iconUrl ? 'url(' + iconUrl + ')' : '' }"></view>
7
+                <text class="title-text">{{ title }}</text>
8
+            </view>
9
+        </template>
10
+        <view ref="contentSlot">
11
+            <slot></slot>
12
+        </view>
13
+    </uni-collapse-item>
14
+</template>
15
+
16
+<script>
17
+export default {
18
+    name: 'eikonCollapseItem',
19
+    props: {
20
+        // 折叠面板名称
21
+        name: {
22
+            type: String,
23
+            default: ''
24
+        },
25
+        // 标题文本
26
+        title: {
27
+            type: String,
28
+            default: ''
29
+        },
30
+        // 是否显示动画
31
+        showAnimation: {
32
+            type: Boolean,
33
+            default: true
34
+        },
35
+        // 是否禁用
36
+        disabled: {
37
+            type: Boolean,
38
+            default: false
39
+        },
40
+        // 图标URL地址(可选)
41
+        iconUrl: {
42
+            type: String,
43
+            default: ''
44
+        },
45
+        
46
+    },
47
+    mounted() {
48
+       
49
+    },
50
+    methods: {
51
+        handleCollapseChange(e) {
52
+            // 当折叠面板状态变化时,如果是展开状态,重新计算高度
53
+            if (e.detail.show) {
54
+                this.$nextTick(() => {
55
+                    this.updateCollapseHeight();
56
+                });
57
+            }
58
+        },
59
+      
60
+        updateCollapseHeight() {
61
+            
62
+            // 强制重新渲染折叠面板
63
+            if (this.$refs.collapseItem && this.$refs.collapseItem.$el) {
64
+                // 触发uni-collapse-item的重新渲染
65
+                const collapseItem = this.$refs.collapseItem;
66
+                if (collapseItem.isOpen) {
67
+                    // 如果是展开状态,强制重新计算高度
68
+                    this.$nextTick(() => {
69
+                        if (collapseItem.resize) {
70
+                            collapseItem.resize();
71
+                        } else {
72
+                            // 如果resize方法不存在,使用uni-app的节点查询获取高度
73
+                            const query = uni.createSelectorQuery().in(this);
74
+                            query.select('.uni-collapse-item__wrap').boundingClientRect(data => {
75
+                                debugger
76
+                                if (data) {
77
+                                    // 获取内容区域的实际高度 - 使用ref而不是ID选择器
78
+                                    const contentQuery = uni.createSelectorQuery().in(this);
79
+                                    contentQuery.select('.uni-collapse-item__wrap-content').boundingClientRect(contentData => {
80
+                                        if (contentData && contentData.height > 0) {
81
+                                            // 设置折叠面板高度为内容高度
82
+                                            const wrapElement = collapseItem.$el.querySelector('.uni-collapse-item__wrap');
83
+                                            if (wrapElement) {
84
+                                                wrapElement.style.height = contentData.height + 'px';
85
+                                            }
86
+                                        }
87
+                                    }).exec();
88
+                                }
89
+                            }).exec();
90
+                        }
91
+                    });
92
+                }
93
+            }
94
+        }
95
+    },
96
+    beforeDestroy() {
97
+        if (this.contentObserver) {
98
+            this.contentObserver.disconnect();
99
+        }
100
+    }
101
+}
102
+</script>
103
+
104
+<style lang="scss" scoped>
105
+::v-deep .uni-collapse-item__title {
106
+    height: 96rpx !important;
107
+    padding-left: 28rpx;
108
+}
109
+
110
+// 确保内容区域在展开时高度自适应
111
+::v-deep .uni-collapse-item__wrap {
112
+    // 保持原有行为,不设置固定高度
113
+}
114
+
115
+::v-deep .uni-collapse-item__content {
116
+    // 移除任何可能影响动画的高度限制
117
+    // 让uni-collapse-item自己管理高度
118
+}
119
+
120
+.h-collapse-item-title {
121
+    display: flex;
122
+    align-items: center;
123
+
124
+    .title-icon {
125
+        width: 40rpx;
126
+        height: 40rpx;
127
+        margin-right: 8px;
128
+        background-size: contain;
129
+        background-repeat: no-repeat;
130
+        background-position: center;
131
+    }
132
+
133
+    .title-text {
134
+        font-size: 14px;
135
+        color: #333;
136
+    }
137
+}
138
+</style>

+ 74 - 0
src/components/h-legend/h-legend.vue

@@ -0,0 +1,74 @@
1
+<template>
2
+  <view class="h-legend-container">
3
+    <view v-for="(item, index) in legendData" :key="index" class="legend-item"
4
+      :style="{ marginRight: index === legendData.length - 1 ? '0' : itemSpacing + 'rpx' }">
5
+      <view class="legend-color" :style="{ backgroundColor: item.color }"></view>
6
+      <text class="legend-text" :style="{ fontSize: textSize + 'rpx', color: textColor }">
7
+        {{ item.name }}
8
+      </text>
9
+    </view>
10
+  </view>
11
+</template>
12
+
13
+<script>
14
+export default {
15
+  name: 'HLegend',
16
+  props: {
17
+    // 图例数据数组
18
+    legendData: {
19
+      type: Array,
20
+      default: () => []
21
+    },
22
+    // 图例颜色块大小
23
+    colorSize: {
24
+      type: Number,
25
+      default: 24
26
+    },
27
+    // 文字大小
28
+    textSize: {
29
+      type: Number,
30
+      default: 24
31
+    },
32
+    // 文字颜色
33
+    textColor: {
34
+      type: String,
35
+      default: '#333333'
36
+    },
37
+    // 图例间距
38
+    itemSpacing: {
39
+      type: Number,
40
+      default: 32
41
+    }
42
+  }
43
+}
44
+</script>
45
+
46
+<style lang="scss" scoped>
47
+.h-legend-container {
48
+  display: flex;
49
+  flex-wrap: nowrap;
50
+  align-items: center;
51
+  overflow-x: auto;
52
+  padding: 16rpx 0;
53
+}
54
+
55
+.legend-item {
56
+  display: flex;
57
+  align-items: center;
58
+  flex-shrink: 0;
59
+}
60
+
61
+.legend-color {
62
+  border-radius: 4rpx;
63
+  margin-right: 12rpx;
64
+  margin-top: 2rpx;
65
+  flex-shrink: 0;
66
+  width: 45rpx;
67
+  height: 22rpx;
68
+}
69
+
70
+.legend-text {
71
+  font-weight: 400;
72
+  white-space: nowrap;
73
+}
74
+</style>

+ 200 - 0
src/components/h-line/h-line.vue

@@ -0,0 +1,200 @@
1
+<template>
2
+  <view class="h-line-container">
3
+    <!-- 单个长条容器 -->
4
+    <view class="single-line-container" :style="{
5
+      height: lineHeight + 'rpx'
6
+    }">
7
+      <!-- 背景条 -->
8
+      <view class="line-background" :style="{
9
+        borderRadius: borderRadius + 'rpx',
10
+        backgroundColor: backgroundColor
11
+      }"></view>
12
+
13
+      <!-- 背景条 -->
14
+      <view class="line-background" :style="{
15
+        borderRadius: borderRadius + 'rpx',
16
+        backgroundColor: backgroundColor
17
+      }"></view>
18
+
19
+      <!-- 分段进度条 -->
20
+      <view v-for="(item, index) in data" :key="index" class="segment-progress" :style="{
21
+        width: calculateSegmentWidth(item) + '%',
22
+        left: calculateSegmentLeft(index) + '%',
23
+        backgroundColor: item.color || defaultColor,
24
+        borderRadius: getSegmentBorderRadius(index) + 'rpx'
25
+      }">
26
+        <!-- 每个分段的数值显示 -->
27
+        <text v-if="item.value > 0" class="segment-text">
28
+          {{ item.value }}
29
+        </text>
30
+      </view>
31
+    </view>
32
+  </view>
33
+</template>
34
+
35
+<script>
36
+export default {
37
+  name: 'HLine',
38
+  props: {
39
+    // 数据数组
40
+    data: {
41
+      type: Array,
42
+      default: () => []
43
+    },
44
+    // 总数量(用于计算百分比)
45
+    total: {
46
+      type: Number,
47
+      default: 0
48
+    },
49
+    // 条宽度
50
+    itemWidth: {
51
+      type: Number,
52
+      default: 200
53
+    },
54
+    // 条高度
55
+    lineHeight: {
56
+      type: String,
57
+      default: '40'
58
+    },
59
+    // 条间距
60
+    itemSpacing: {
61
+      type: Number,
62
+      default: 32
63
+    },
64
+    // 圆角大小
65
+    borderRadius: {
66
+      type: Number,
67
+      default: 20
68
+    },
69
+    // 背景颜色
70
+    backgroundColor: {
71
+      type: String,
72
+      default: '#f5f5f5'
73
+    },
74
+    // 默认进度条颜色
75
+    defaultColor: {
76
+      type: String,
77
+      default: '#4873E3'
78
+    },
79
+    // 是否显示百分比
80
+    showPercentage: {
81
+      type: Boolean,
82
+      default: true
83
+    },
84
+ 
85
+  
86
+    // 百分比文字大小
87
+    percentageSize: {
88
+      type: Number,
89
+      default: 24
90
+    },
91
+    // 百分比文字颜色
92
+    percentageColor: {
93
+      type: String,
94
+      default: '#666666'
95
+    }
96
+  },
97
+  computed: {
98
+    // 计算总数量(如果未传入total,则使用data中所有值的和)
99
+    computedTotal() {
100
+      if (this.total > 0) {
101
+        return this.total;
102
+      }
103
+      return this.data.reduce((sum, item) => sum + (item.value || 0), 0);
104
+    }
105
+  },
106
+  methods: {
107
+    // 计算每个分段的宽度百分比
108
+    calculateSegmentWidth(item) {
109
+      if (this.computedTotal === 0) return 0;
110
+      const percentage = (item.value / this.computedTotal) * 100;
111
+      // 确保最小宽度为1%,以便显示
112
+      return Math.max(1, percentage);
113
+    },
114
+
115
+    // 计算每个分段的左侧位置
116
+    calculateSegmentLeft(index) {
117
+      if (index === 0) return 0;
118
+      let left = 0;
119
+      for (let i = 0; i < index; i++) {
120
+        left += this.calculateSegmentWidth(this.data[i]);
121
+      }
122
+      return left;
123
+    },
124
+
125
+    // 获取分段圆角样式
126
+    getSegmentBorderRadius(index) {
127
+      if (this.data.length === 1) {
128
+        return this.borderRadius;
129
+      }
130
+      if (index === 0) {
131
+        return `${this.borderRadius} 0 0 ${this.borderRadius}`;
132
+      }
133
+      if (index === this.data.length - 1) {
134
+        return `0 ${this.borderRadius} ${this.borderRadius} 0`;
135
+      }
136
+      return 0;
137
+    },
138
+
139
+    // 计算总百分比
140
+    calculateTotalPercentage() {
141
+      if (this.computedTotal === 0) return 0;
142
+      return Math.round((this.computedTotal / this.total) * 100);
143
+    }
144
+  }
145
+}
146
+</script>
147
+
148
+<style lang="scss" scoped>
149
+.h-line-container {
150
+  display: flex;
151
+  align-items: center;
152
+  padding: 8rpx 0;
153
+  width: 100%;
154
+}
155
+
156
+.single-line-container {
157
+  position: relative;
158
+  display: flex;
159
+  width: 100%;
160
+  align-items: center;
161
+  justify-content: center;
162
+  overflow: hidden;
163
+  border-radius: 20rpx;
164
+}
165
+
166
+.line-background {
167
+  position: absolute;
168
+  top: 0;
169
+  left: 0;
170
+  width: 100%;
171
+  height: 100%;
172
+  z-index: 1;
173
+}
174
+
175
+.segment-progress {
176
+  position: absolute;
177
+  top: 0;
178
+  height: 100%;
179
+  z-index: 2;
180
+  transition: width 0.3s ease;
181
+}
182
+
183
+.segment-progress {
184
+  position: absolute;
185
+  top: 0;
186
+  height: 100%;
187
+  z-index: 2;
188
+  transition: width 0.3s ease;
189
+  display: flex;
190
+  align-items: center;
191
+  justify-content: center;
192
+}
193
+
194
+.segment-text {
195
+  font-size: 23rpx;
196
+  font-weight: 500;
197
+  color: #ffffff;
198
+  text-shadow: 0 0 2rpx rgba(0, 0, 0, 0.5);
199
+}
200
+</style>

+ 104 - 0
src/components/h-rank-line/h-rank-line.vue

@@ -0,0 +1,104 @@
1
+<template>
2
+  <view class="h-rank-line-container">
3
+    <!-- 左侧进度条区域 -->
4
+    <view class="progress-section">
5
+      <!-- 灰条背景 -->
6
+      <view class="gray-bar">
7
+        <!-- 蓝条,根据百分比显示 -->
8
+        <view class="blue-bar" :style="{ width: percentage + '%' }">
9
+          <!-- 蓝条结尾的竖条 -->
10
+          <view class="end-line"></view>
11
+        </view>
12
+      </view>
13
+    </view>
14
+
15
+    <!-- 右侧插槽区域 -->
16
+    <view class="slot-section">
17
+      <slot></slot>
18
+    </view>
19
+  </view>
20
+</template>
21
+
22
+<script>
23
+export default {
24
+  name: 'HRankLine',
25
+  props: {
26
+    // 百分比值,范围0-100
27
+    percentage: {
28
+      type: Number,
29
+      default: 0,
30
+      validator: (value) => {
31
+        return value >= 0 && value <= 100
32
+      }
33
+    }
34
+  },
35
+  data() {
36
+    return {}
37
+  }
38
+}
39
+</script>
40
+
41
+<style lang="scss" scoped>
42
+.h-rank-line-container {
43
+  display: flex;
44
+  align-items: center;
45
+  width: 100%;
46
+  height: 60rpx;
47
+
48
+}
49
+
50
+.progress-section {
51
+  flex: 1;
52
+  margin-right: 20rpx;
53
+}
54
+
55
+.gray-bar {
56
+  position: relative;
57
+  width: 100%;
58
+  height: 24rpx;
59
+  background: #f0f0f0;
60
+  border-radius: 12rpx;
61
+  // overflow: hidden;
62
+}
63
+
64
+.blue-bar {
65
+  position: relative;
66
+  height: 100%;
67
+  background: linear-gradient(90deg, #0098FA 0%, #0098FA 100%);
68
+  border-radius: 12rpx 0 0 12rpx;
69
+  transition: width 0.3s ease;
70
+  display: flex;
71
+  align-items: center;
72
+  justify-content: flex-end;
73
+}
74
+
75
+.end-line {
76
+  width: 4rpx;
77
+  height: 35rpx;
78
+  background: #0098FA;
79
+  border-radius: 2rpx;
80
+  box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
81
+  margin-right: -2rpx;
82
+}
83
+
84
+.slot-section {
85
+  flex-shrink: 0;
86
+  min-width: 100rpx;
87
+  text-align: right;
88
+}
89
+
90
+/* 响应式适配 */
91
+@media (max-width: 768px) {
92
+  .h-rank-line-container {
93
+    height: 50rpx;
94
+  }
95
+
96
+  .gray-bar {
97
+    height: 20rpx;
98
+  }
99
+
100
+  .end-line {
101
+    height: 35rpx;
102
+  }
103
+}
104
+</style>

+ 128 - 0
src/components/h-search/h-search.vue

@@ -0,0 +1,128 @@
1
+<template>
2
+    <u-search v-model="internalValue" :placeholder="placeholder" :showAction="showAction" :actionText="actionText"
3
+        @search="handleSearch" @custom="handleCustom" @clear="handleClear" :disabled="disabled" :maxlength="maxlength"
4
+        :height="height" :bgColor="bgColor" :color="color" :searchIconColor="searchIconColor"
5
+        :clearButton="clearButton" />
6
+</template>
7
+
8
+<script>
9
+export default {
10
+    name: 'HSearch',
11
+    props: {
12
+        // 双向绑定的值
13
+        value: {
14
+            type: String,
15
+            default: ''
16
+        },
17
+        // 占位符文本
18
+        placeholder: {
19
+            type: String,
20
+            default: '请输入关键词'
21
+        },
22
+        // 是否显示搜索按钮
23
+        showAction: {
24
+            type: Boolean,
25
+            default: true
26
+        },
27
+        // 搜索按钮文本
28
+        actionText: {
29
+            type: String,
30
+            default: '搜索'
31
+        },
32
+        // 是否禁用
33
+        disabled: {
34
+            type: Boolean,
35
+            default: false
36
+        },
37
+        // 最大输入长度
38
+        maxlength: {
39
+            type: Number,
40
+            default: 100
41
+        },
42
+        // 搜索框高度
43
+        height: {
44
+            type: Number,
45
+            default: 64
46
+        },
47
+        // 背景颜色
48
+        bgColor: {
49
+            type: String,
50
+            default: '#f5f5f5'
51
+        },
52
+        // 文字颜色
53
+        color: {
54
+            type: String,
55
+            default: '#333333'
56
+        },
57
+        // 搜索图标颜色
58
+        searchIconColor: {
59
+            type: String,
60
+            default: '#999999'
61
+        },
62
+        // 是否显示清除按钮
63
+        clearButton: {
64
+            type: Boolean,
65
+            default: true
66
+        }
67
+    },
68
+    data() {
69
+        return {
70
+            internalValue: this.value
71
+        };
72
+    },
73
+    watch: {
74
+        value(newVal) {
75
+            this.internalValue = newVal;
76
+        },
77
+        internalValue(newVal) {
78
+            this.$emit('input', newVal);
79
+            this.$emit('update:value', newVal);
80
+        }
81
+    },
82
+    methods: {
83
+        // 处理搜索事件
84
+        handleSearch(value) {
85
+            this.$emit('search', value);
86
+        },
87
+        // 处理自定义事件(点击搜索按钮)
88
+        handleCustom(value) {
89
+            this.$emit('custom', value);
90
+        },
91
+        // 处理清除事件
92
+        handleClear() {
93
+            this.internalValue = '';
94
+            this.$emit('clear');
95
+            this.$emit('search', '');
96
+        },
97
+        // 聚焦搜索框
98
+        focus() {
99
+            this.$refs.uSearch && this.$refs.uSearch.focus();
100
+        },
101
+        // 失焦搜索框
102
+        blur() {
103
+            this.$refs.uSearch && this.$refs.uSearch.blur();
104
+        },
105
+        // 清空搜索框
106
+        clear() {
107
+            this.internalValue = '';
108
+            this.handleClear();
109
+        }
110
+    }
111
+};
112
+</script>
113
+
114
+<style lang="scss" scoped>
115
+// 可以在这里添加自定义样式
116
+.h-search-container {
117
+    width: 100%;
118
+}
119
+
120
+::v-deep .u-search__content {
121
+
122
+
123
+    .u-search__content__input {
124
+        height: 80rpx !important;
125
+        
126
+    }
127
+}
128
+</style>

+ 105 - 0
src/components/h-tabs/h-tabs.vue

@@ -0,0 +1,105 @@
1
+<template>
2
+  <view class="h-tabs" :style="{ '--active-color': activeColor }">
3
+    <view v-for="item in tabs" :key="item[valueKey]" :class="{ active: item[valueKey] === currentValue }"
4
+      :style="[item[valueKey] === currentValue ? item.activeStyle : item.style]" class="h-tabs-item"
5
+      @click="handleTabClick(item)">
6
+      {{ item[labelKey] }}
7
+      <!-- 显示红点 -->
8
+      <view v-if="item.unreadCount > 0" class="h-tabs-red-dot"></view>
9
+    </view>
10
+  </view>
11
+</template>
12
+
13
+<script>
14
+export default {
15
+  name: 'HTabs',
16
+  props: {
17
+    tabs: {
18
+      type: Array,
19
+      default: () => []
20
+    },
21
+    value: {
22
+      type: [String, Number],
23
+      default: ''
24
+    },
25
+    valueKey: {
26
+      type: String,
27
+      default: 'type'
28
+    },
29
+    labelKey: {
30
+      type: String,
31
+      default: 'title'
32
+    },
33
+    activeColor: {
34
+      type: String,
35
+      default: '#2A70D1'
36
+    }
37
+  },
38
+  data() {
39
+    return {
40
+      currentValue: this.value
41
+    };
42
+  },
43
+  watch: {
44
+    value(newVal) {
45
+      this.currentValue = newVal;
46
+    }
47
+  },
48
+  methods: {
49
+    handleTabClick(item) {
50
+      this.currentValue = item[this.valueKey];
51
+      this.$emit('input', this.currentValue);
52
+      this.$emit('change', this.currentValue, item);
53
+    }
54
+  }
55
+};
56
+</script>
57
+
58
+<style lang="scss" scoped>
59
+.h-tabs {
60
+  display: flex;
61
+  padding: 0 0 32rpx;
62
+  font-size: 28rpx;
63
+  color: #666666;
64
+  line-height: 32rpx;
65
+  gap: 48rpx;
66
+  width: 100%;
67
+
68
+  // 默认样式
69
+  .h-tabs-item {
70
+    position: relative;
71
+    cursor: pointer;
72
+    padding: 0 8rpx;
73
+
74
+    &.active {
75
+      font-size: 36rpx;
76
+      color: var(--active-color);
77
+      line-height: 42rpx;
78
+
79
+      &:after {
80
+        content: "";
81
+        position: absolute;
82
+        bottom: -12rpx;
83
+        left: 0;
84
+        width: 50%;
85
+        height: 6rpx;
86
+        background: var(--active-color);
87
+        border-radius: 12rpx;
88
+        transition: all 0.3s ease;
89
+      }
90
+    }
91
+  }
92
+
93
+  // 红点样式
94
+  .h-tabs-red-dot {
95
+    position: absolute;
96
+    top: -4rpx;
97
+    right: 0;
98
+    width: 16rpx;
99
+    height: 16rpx;
100
+    background-color: #FF4D4F;
101
+    border-radius: 50%;
102
+    transform: translateX(50%);
103
+  }
104
+}
105
+</style>

+ 81 - 0
src/components/list-card/list-card.vue

@@ -0,0 +1,81 @@
1
+<template>
2
+  <view class="list-card-container" @click="handleClick" :class="containerStyle">
3
+    <slot v-if="showChecked" name="checkbox">
4
+
5
+    </slot>
6
+    <view class="list-card">
7
+      <view class="list-card-header">
8
+        <slot name="title"></slot>
9
+        <slot name="icon"></slot>
10
+      </view>
11
+      <view class="list-card-content">
12
+        <slot></slot>
13
+      </view>
14
+
15
+    </view>
16
+    <slot name="footer"></slot>
17
+  </view>
18
+
19
+</template>
20
+
21
+<script>
22
+export default {
23
+  name: 'ListCard',
24
+  props: {
25
+    showChecked: {
26
+      type: Boolean,
27
+      default: false
28
+    },
29
+    containerStyle: {
30
+      type: Object,
31
+      default: () => ({})
32
+    }
33
+  },
34
+  data() {
35
+    return {
36
+      checked: false
37
+    }
38
+  },
39
+
40
+  methods: {
41
+    handleClick() {
42
+      this.$emit('click');
43
+    }
44
+  }
45
+}
46
+</script>
47
+
48
+<style lang="scss" scoped>
49
+.list-card-container {
50
+  background: #FFFFFF;
51
+  border-radius: 16rpx;
52
+  box-shadow: 0 8rpx 20rpx 0 rgba(0, 0, 0, 0.08);
53
+  border: 2rpx solid #F0F8FF;
54
+  padding: 32rpx;
55
+  margin-bottom: 32rpx;
56
+  display: flex;
57
+  align-items: flex-start;
58
+  position: relative;
59
+
60
+
61
+
62
+  .list-card {
63
+    width: 100%;
64
+
65
+    .list-card-header {
66
+      display: flex;
67
+      justify-content: space-between;
68
+      align-items: center;
69
+      margin-bottom: 24rpx;
70
+
71
+      .list-card-checkbox {
72
+        margin-right: 16rpx;
73
+      }
74
+    }
75
+
76
+    .list-card-content {
77
+      // 默认内容样式
78
+    }
79
+  }
80
+}
81
+</style>

+ 38 - 0
src/components/list-icon/list-icon.vue

@@ -0,0 +1,38 @@
1
+<template>
2
+  <view class="list-icon" :style="divStyle">
3
+    {{ text }}
4
+  </view>
5
+</template>
6
+
7
+<script>
8
+export default {
9
+  name: 'ListIcon',
10
+  props: {
11
+    divStyle: {
12
+      type: Object,
13
+      default: () => ({
14
+        backgroundColor: '#5972C0'
15
+      })
16
+    },
17
+    text: {
18
+      type: String,
19
+      default: ''
20
+    }
21
+  }
22
+}
23
+</script>
24
+
25
+<style lang="scss" scoped>
26
+.list-icon {
27
+  display: inline-flex;
28
+  align-items: center;
29
+  justify-content: center;
30
+  min-width: 60rpx;
31
+  height: 40rpx;
32
+  padding: 0 16rpx;
33
+  border-radius: 8rpx;
34
+  font-size: 24rpx;
35
+  color: #FFFFFF;
36
+  line-height: 1;
37
+}
38
+</style>

+ 134 - 0
src/components/list-user/list-user.vue

@@ -0,0 +1,134 @@
1
+<template>
2
+  <view class="personnel-list" v-if="data && data.length">
3
+    <view class="personnel-item" v-for="(item, index) of data" :key="item.id || index" @click="handleItemClick(item, index)">
4
+      <view class="personnel-img">
5
+        <UserAvatar :userName="item.nickName || item.userName" :avatarLink="item.avatar" />
6
+      </view>
7
+      <view class="personnel-name">{{ item.nickName || item.userName }}</view>
8
+      <view v-if="showDelete" class="delete-icon" @click.stop="handleDelete(index)">
9
+        <uni-icons type="close" size="16" color="#999"></uni-icons>
10
+      </view>
11
+    </view>
12
+  </view>
13
+</template>
14
+
15
+<script>
16
+import UserAvatar from '@/pages/attendance/components/UserAvatar.vue'
17
+
18
+export default {
19
+  name: 'ListUser',
20
+  components: {
21
+    UserAvatar
22
+  },
23
+  props: {
24
+    // 人员数据数组
25
+    data: {
26
+      type: Array,
27
+      default: () => []
28
+    },
29
+    // 是否显示删除图标
30
+    showDelete: {
31
+      type: Boolean,
32
+      default: false
33
+    },
34
+    // 禁用状态
35
+    disabled: {
36
+      type: Boolean,
37
+      default: false
38
+    }
39
+  },
40
+  methods: {
41
+    // 处理删除操作
42
+    handleDelete(index) {
43
+      if (!this.data || !this.data.length || this.disabled) return
44
+
45
+      // 创建新数组(避免直接修改props)
46
+      const newData = [...this.data]
47
+      const deletedItem = newData.splice(index, 1)[0]
48
+
49
+      // 向外传递删除后的数组和删除的项
50
+      this.$emit('update:data', newData)
51
+      this.$emit('delete', {
52
+        deletedItem,
53
+        index,
54
+        newData
55
+      })
56
+
57
+      uni.showToast({
58
+        title: '删除成功',
59
+        icon: 'success',
60
+        duration: 1500
61
+      })
62
+    },
63
+
64
+    // 处理人员项点击
65
+    handleItemClick(item, index) {
66
+      this.$emit('click', {
67
+        clickedItem: item,
68
+        index,
69
+        data: this.data
70
+      })
71
+    }
72
+  }
73
+}
74
+</script>
75
+
76
+<style lang="scss" scoped>
77
+.personnel-list {
78
+  display: flex;
79
+  flex-wrap: wrap;
80
+  gap: 16rpx;
81
+  margin: 20rpx 0;
82
+}
83
+
84
+.personnel-item {
85
+  display: flex;
86
+  align-items: center;
87
+  background: #f8f9fa;
88
+  border-radius: 24rpx;
89
+  padding: 12rpx 20rpx;
90
+  position: relative;
91
+
92
+  .personnel-img {
93
+    width: 60rpx;
94
+    height: 60rpx;
95
+    border-radius: 50%;
96
+    overflow: hidden;
97
+    margin-right: 16rpx;
98
+    flex-shrink: 0;
99
+  }
100
+
101
+  .personnel-name {
102
+    font-size: 28rpx;
103
+    color: #333;
104
+    font-weight: 500;
105
+    flex: 1;
106
+    min-width: 0;
107
+    overflow: hidden;
108
+    text-overflow: ellipsis;
109
+    white-space: nowrap;
110
+  }
111
+
112
+  .delete-icon {
113
+    width: 40rpx;
114
+    height: 40rpx;
115
+    border-radius: 50%;
116
+    background: #f0f0f0;
117
+    display: flex;
118
+    align-items: center;
119
+    justify-content: center;
120
+    margin-left: 16rpx;
121
+    flex-shrink: 0;
122
+    cursor: pointer;
123
+    transition: background-color 0.2s;
124
+
125
+    &:hover {
126
+      background: #e0e0e0;
127
+    }
128
+
129
+    &:active {
130
+      background: #d0d0d0;
131
+    }
132
+  }
133
+}
134
+</style>

+ 75 - 0
src/components/no-data/no-data.vue

@@ -0,0 +1,75 @@
1
+<template>
2
+  <view class="no-data-container">
3
+    <!-- 图标 -->
4
+    <view class="no-data-icon">
5
+      <uni-icons 
6
+        :type="iconType" 
7
+        :size="iconSize" 
8
+        :color="iconColor" 
9
+      />
10
+    </view>
11
+    
12
+    <!-- 提示文字 -->
13
+    <text class="no-data-text">
14
+      {{ text }}
15
+    </text>
16
+  </view>
17
+</template>
18
+
19
+<script>
20
+// 导入uni-ui图标组件
21
+import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue';
22
+
23
+export default {
24
+  name: 'NoData',
25
+  components: {
26
+    uniIcons
27
+  },
28
+  props: {
29
+    // 提示文字
30
+    text: {
31
+      type: String,
32
+      default: '暂无数据'
33
+    },
34
+    // 图标类型
35
+    iconType: {
36
+      type: String,
37
+      default: 'search' // 可以使用其他图标如: 'info', 'help', 'minus', etc.
38
+    },
39
+    // 图标大小
40
+    iconSize: {
41
+      type: [Number, String],
42
+      default: 60
43
+    },
44
+    // 图标颜色
45
+    iconColor: {
46
+      type: String,
47
+      default: '#C0C4CC'
48
+    }
49
+  }
50
+}
51
+</script>
52
+
53
+<style lang="scss" scoped>
54
+.no-data-container {
55
+  display: flex;
56
+  flex-direction: column;
57
+  align-items: center;
58
+  justify-content: center;
59
+  width: 100%;
60
+  height: 100%;
61
+  text-align: center;
62
+  padding: 40rpx 0; /* 添加上下padding值 */
63
+  
64
+  .no-data-icon {
65
+    margin-bottom: 20rpx;
66
+    opacity: 0.5;
67
+  }
68
+  
69
+  .no-data-text {
70
+    font-size: 28rpx;
71
+    color: #909399;
72
+    font-weight: 400;
73
+  }
74
+}
75
+</style>

+ 81 - 0
src/components/select-tag/select-tag.vue

@@ -0,0 +1,81 @@
1
+<template>
2
+    <div class="time-tag-container">
3
+        <div class="custom-tag-container">
4
+            <div v-for="tag in tags" :key="tag.value" class="custom-tag"
5
+                :class="{ 'active': selectedValue === tag.value }" @click="handleTagClick(tag)">
6
+                <span class="tag-text">{{ tag.label }}</span>
7
+            </div>
8
+        </div>
9
+    </div>
10
+</template>
11
+
12
+<script>
13
+export default {
14
+    name: 'SelectTag',
15
+    props: {
16
+        tags: {
17
+            type: Array,
18
+            default: () => []
19
+        },
20
+        selectedValue: {
21
+            type: String,
22
+            default: ''
23
+        }
24
+    },
25
+    methods: {
26
+        handleTagClick(tag) {
27
+            if (tag.value === 'custom') {
28
+                // 触发自定义时间选择器显示事件
29
+                this.$emit('show-custom-picker', tag.value);
30
+            } else {
31
+                // 触发普通时间范围变化事件
32
+                this.$emit('change', tag.value);
33
+            }
34
+        }
35
+    }
36
+}
37
+</script>
38
+
39
+<style lang="scss" scoped>
40
+.time-tag-container {
41
+    display: flex;
42
+    justify-content: flex-start;
43
+    margin-top: 30rpx;
44
+}
45
+
46
+.custom-tag-container {
47
+    display: flex;
48
+    flex-wrap: wrap;
49
+    gap: 5rpx;
50
+    align-items: center;
51
+}
52
+
53
+.custom-tag {
54
+    display: flex;
55
+    align-items: center;
56
+    padding: 7rpx 15rpx;
57
+
58
+    color: #FFFFFF;
59
+
60
+    border-radius: 20rpx;
61
+    font-size: 24rpx;
62
+    cursor: pointer;
63
+    transition: all 0.3s ease;
64
+}
65
+
66
+.custom-tag.active {
67
+    background: rgba(255, 255, 255, 0.2);
68
+    color: #FFFFFF;
69
+    font-weight: bold;
70
+    background: rgba(255, 255, 255, 0.2);
71
+}
72
+
73
+// .custom-tag:hover {
74
+//     background: rgba(255, 255, 255, 0.4);
75
+//     border-color: rgba(255, 255, 255, 0.6);
76
+// }
77
+
78
+.tag-text {
79
+    // margin-right: 8rpx;
80
+}
81
+</style>

+ 136 - 0
src/components/statistic-table/statistic-table.vue

@@ -0,0 +1,136 @@
1
+<template>
2
+  <div class="table-container">
3
+    <table class="data-table">
4
+      <thead>
5
+        <tr>
6
+          <th v-for="column in columns" :key="column.props">{{ column.title }}</th>
7
+        </tr>
8
+      </thead>
9
+      <tbody>
10
+        <tr v-for="(row, index) in data" :key="index"
11
+          :class="{ 'odd-row': (index + 1) % 2 !== 0, 'even-row': (index + 1) % 2 === 0 }">
12
+          
13
+          <td v-for="column in columns" :key="`${column.props}-${index}`">
14
+            <!-- 如果列配置了slot属性,则使用插槽 -->
15
+            <slot v-if="column.slot" :name="`column-${column.props}`" :row="row" :index="index">
16
+              <!-- 默认插槽内容 -->
17
+              {{ row[column.props] }}
18
+            </slot>
19
+            <!-- 如果没有配置slot属性,则显示普通文本 -->
20
+            <template v-else>
21
+              {{ row[column.props] || '-' }}
22
+            </template>
23
+          </td>
24
+        </tr>
25
+      </tbody>
26
+    </table>
27
+  </div>
28
+</template>
29
+
30
+<script>
31
+export default {
32
+  name: 'TableList',
33
+  props: {
34
+    columns: {
35
+      type: Array,
36
+      default: () => [],
37
+      validator: (value) => {
38
+        return value.every(column => {
39
+          return column.props && column.title;
40
+        });
41
+      }
42
+    },
43
+    data: {
44
+      type: Array,
45
+      default: () => []
46
+    }
47
+  },
48
+  created() {
49
+    console.log(this.columns,this.data);
50
+  }
51
+};
52
+</script>
53
+
54
+<style lang="scss" scoped>
55
+.table-container {
56
+  width: 100%;
57
+  overflow-x: auto;
58
+  -webkit-overflow-scrolling: touch;
59
+  
60
+  /* 强制显示滚动条 */
61
+  scrollbar-width: thin; /* Firefox */
62
+  scrollbar-color: #525861 #f1f1f1; /* Firefox */
63
+  
64
+  /* 自定义滚动条样式 - 更明显的滚动条 */
65
+  &::-webkit-scrollbar {
66
+    height: 12px; /* 增加滚动条高度 */
67
+    width: 12px;
68
+  }
69
+  
70
+  &::-webkit-scrollbar-track {
71
+    background: #f1f1f1;
72
+    border-radius: 6px;
73
+    box-shadow: inset 0 0 5px rgba(0,0,0,0.1);
74
+  }
75
+  
76
+  &::-webkit-scrollbar-thumb {
77
+    background: #525861; /* 使用主题色 */
78
+    border-radius: 6px;
79
+    border: 2px solid #f1f1f1;
80
+  }
81
+  
82
+  &::-webkit-scrollbar-thumb:hover {
83
+    background: #525861; /* 悬停时加深颜色 */
84
+  }
85
+  
86
+  &::-webkit-scrollbar-corner {
87
+    background: #f1f1f1;
88
+  }
89
+}
90
+
91
+.data-table {
92
+  width: auto;
93
+  min-width: 100%;
94
+  border-collapse: collapse;
95
+  border: 2rpx solid #ccc;
96
+  border-radius: 16rpx;
97
+  table-layout: auto;
98
+
99
+  th,
100
+  td {
101
+    padding: 12rpx;
102
+    height: 48rpx;
103
+    text-align: left;
104
+    white-space: nowrap;
105
+    overflow: visible;
106
+    text-overflow: clip;
107
+    // border-bottom: 1rpx solid #ccc;
108
+  }
109
+
110
+  th {
111
+    background-color: #f5f5f5;
112
+    font-weight: bold;
113
+    color: #666;
114
+    position: sticky;
115
+    top: 0;
116
+    z-index: 1;
117
+    
118
+    /* 确保表头内容完全展示 */
119
+    white-space: nowrap;
120
+    overflow: visible;
121
+    text-overflow: clip;
122
+  }
123
+
124
+  .odd-row {
125
+    background-color: #fff !important;
126
+  }
127
+
128
+  .even-row {
129
+    background-color: #f5f5f5 !important;
130
+  }
131
+
132
+  tr:hover {
133
+    background-color: #e0e0e0;
134
+  }
135
+}
136
+</style>

+ 218 - 0
src/components/text-switch/text-switch.vue

@@ -0,0 +1,218 @@
1
+<!-- import textSwitch from "@/components/text-li-switch/text-li-switch.vue" -->
2
+<!-- <textSwitch v-model="prop.must" uncheck_text="非必选" checked_text="必选" @change="switchChange"></textSwitch> -->
3
+<!-- @change可以不写 -->
4
+
5
+
6
+<template>
7
+	<view>
8
+		<view class="switch_box row-b-c" :class="{ 'disabled-switch': disabled }"
9
+			:style="{ backgroundColor: checked ? checkedbg : uncheckedbg, width: width + 'rpx', height: height + 'rpx', borderRadius: '100px' }"
10
+			@click="changeSwitch">
11
+			<view class="switch_text c-white" :class="checked ? ['to_left'] : ['_right', 'c-black']"
12
+				:style="{ fontSize: size + 'rpx', 'padding-right': padding_right }">{{ checked ? checked_text :
13
+					uncheck_text }}
14
+			</view>
15
+			<view class="round" :style="{
16
+				left: checked ? '100%' : '2rpx',
17
+				marginLeft: checked ? '-' + (Number(height) - 6) + 'rpx' : '', width: + height + 'rpx',
18
+				height: + height - 10 + 'rpx',
19
+				width: + height - 10 + 'rpx',
20
+				borderRadius: height / 2 + 'rpx'
21
+			}">
22
+			</view>
23
+			<!-- 禁用遮罩层 -->
24
+			<view v-if="disabled" class="disabled-overlay"></view>
25
+		</view>
26
+	</view>
27
+</template>
28
+
29
+<script>
30
+export default {
31
+	emits: ['click', 'change', 'update:modelValue', 'input'],
32
+	model: {
33
+		prop: 'modelValue',
34
+		event: 'update:modelValue'
35
+	},
36
+
37
+	props: {
38
+		modelValue: [String], // 修改为接收字符串值
39
+		checkedbg: {
40
+			type: String,
41
+			default: '#626FF0'
42
+		},
43
+		uncheckedbg: {
44
+			type: String,
45
+			default: '#F67072'
46
+		},
47
+		width: {
48
+			type: String,
49
+			default: '130'
50
+		},
51
+		height: {
52
+			type: String,
53
+			default: '50'
54
+		},
55
+		uncheck_text: {
56
+			type: String,
57
+			default: ''
58
+		},
59
+		checked_text: {
60
+			type: String,
61
+			default: ''
62
+		},
63
+		size: {
64
+			type: String,
65
+			default: '24'
66
+		},
67
+		checked_value: {
68
+			type: String,
69
+			default: 'QUALIFIED'
70
+		},
71
+		uncheck_value: {
72
+			type: String,
73
+			default: 'UNQUALIFIED'
74
+		},
75
+		disabled: {
76
+			type: Boolean,
77
+			default: false
78
+		}
79
+	},
80
+	data() {
81
+		return {
82
+			checked: false, // 修改为布尔值
83
+			padding_right: '20rpx'
84
+		}
85
+	},
86
+	created() {
87
+		if (this.uncheck_text.length > this.checked_text.length) {
88
+			this.padding_right = '10rpx'
89
+		}
90
+		// 根据传入的值初始化选中状态
91
+		this.updateCheckedState();
92
+	},
93
+	watch: {
94
+		modelValue: {
95
+			handler(newVal) {
96
+				this.updateCheckedState();
97
+			},
98
+			deep: true,
99
+			immediate: true
100
+		},
101
+		checked_value: {
102
+			handler() {
103
+				this.updateCheckedState();
104
+			},
105
+			deep: true
106
+		},
107
+		uncheck_value: {
108
+			handler() {
109
+				this.updateCheckedState();
110
+			},
111
+			deep: true
112
+		}
113
+	},
114
+	methods: {
115
+		// 根据传入的值更新选中状态
116
+		updateCheckedState() {
117
+			if (this.modelValue === this.checked_value) {
118
+				this.checked = true;
119
+			} else if (this.modelValue === this.uncheck_value) {
120
+				this.checked = false;
121
+			}
122
+			// 如果传入的值既不等于checked_value也不等于uncheck_value,保持当前状态
123
+		},
124
+		
125
+		changeSwitch: function (e) {
126
+			// 如果禁用状态,阻止切换
127
+			if (this.disabled) {
128
+				return;
129
+			}
130
+			
131
+			this.checked = !this.checked;
132
+			
133
+			// 根据开关状态确定要传递的值
134
+			const current_value = this.checked ? this.checked_value : this.uncheck_value;
135
+			
136
+			// TODO 兼容 vue2
137
+			this.$emit('input', current_value);
138
+			// TODO 兼容 vue3
139
+			this.$emit('update:modelValue', current_value);
140
+
141
+			this.$emit('change', current_value)
142
+		}
143
+	}
144
+}
145
+</script>
146
+
147
+<style>
148
+.row-c-c {
149
+	display: flex;
150
+	justify-content: center;
151
+	align-items: center;
152
+}
153
+
154
+.c-white {
155
+	color: #FFFFFF;
156
+}
157
+
158
+.c-black {
159
+	color: #FFFFFF;
160
+}
161
+
162
+
163
+/* ---------- */
164
+.switch_box {
165
+	position: relative;
166
+	border: 1rpx solid #EEEEEE;
167
+	background-color: #F6F6F6;
168
+	transition: all 0.4s;
169
+	display: flex;
170
+	align-items: center;
171
+}
172
+
173
+.round {
174
+	position: absolute;
175
+	background-color: #FFFFFF;
176
+	transition: all 0.4s;
177
+}
178
+
179
+.to_left {
180
+	left: 0;
181
+	margin-left: 0;
182
+}
183
+
184
+.to_right {
185
+	left: 100%;
186
+	margin-left: -50rpx;
187
+}
188
+
189
+.switch_text {
190
+	position: absolute;
191
+	padding: 0 20rpx;
192
+}
193
+
194
+._right {
195
+	right: 0;
196
+	border-right: none !important;
197
+}
198
+
199
+/* 禁用状态样式 */
200
+.disabled-switch {
201
+	pointer-events: none;
202
+	user-select: none;
203
+	opacity: 0.6;
204
+	position: relative;
205
+}
206
+
207
+.disabled-overlay {
208
+	position: absolute;
209
+	top: 0;
210
+	left: 0;
211
+	right: 0;
212
+	bottom: 0;
213
+	background: rgba(128, 128, 128, 0.3);
214
+	border-radius: 100px;
215
+	z-index: 10;
216
+	pointer-events: none;
217
+}
218
+</style>

+ 113 - 0
src/components/text-tag/text-tag.vue

@@ -0,0 +1,113 @@
1
+<template>
2
+    <view class="text-tag-container">
3
+        <view v-for="(item, index) in localTags" :key="index" class="tag-item" :style="getTagItemStyle(item)">
4
+            <text class="tag-text" :style="getTagTextStyle(item)">{{ item[labelColumn] }}</text>
5
+
6
+            <u-icon v-if="showClose" name="close" color="#666666" size="14" @click="handleRemove(index)" />
7
+        </view>
8
+    </view>
9
+</template>
10
+
11
+<script>
12
+export default {
13
+    name: 'TextTag',
14
+    props: {
15
+        tags: {
16
+            type: Array,
17
+            default: () => []
18
+        },
19
+        showClose: {
20
+            type: Boolean,
21
+            default: true
22
+        },
23
+        labelColumn: {
24
+            type: String,
25
+            default: 'label'
26
+        },
27
+        // 全局tag-item样式配置
28
+        tagItemStyle: {
29
+            type: Object,
30
+            default: () => ({})
31
+        },
32
+        // 全局tag-text样式配置
33
+        tagTextStyle: {
34
+            type: Object,
35
+            default: () => ({})
36
+        }
37
+    },
38
+    data() {
39
+        return {
40
+            localTags: [...this.tags]
41
+        };
42
+    },
43
+    watch: {
44
+        tags(newVal) {
45
+            
46
+            this.localTags = [...newVal];
47
+        }
48
+    },
49
+    methods: {
50
+        handleRemove(index) {
51
+            this.localTags.splice(index, 1);
52
+            this.$emit('remove', [...this.localTags]);
53
+         
54
+        },
55
+        // 获取tag-item样式
56
+        getTagItemStyle(item) {
57
+            const baseStyle = {
58
+                display: 'flex',
59
+                alignItems: 'center',
60
+                padding: '4px 12px',
61
+                backgroundColor: '#f0f0f0',
62
+                borderRadius: '16px',
63
+                fontSize: '14px'
64
+            };
65
+            
66
+            // 合并全局样式和item中的样式
67
+            const mergedStyle = {
68
+                ...baseStyle,
69
+                ...this.tagItemStyle,
70
+                ...(item.tagItemStyle || {})
71
+            };
72
+            
73
+            return mergedStyle;
74
+        },
75
+        // 获取tag-text样式
76
+        getTagTextStyle(item) {
77
+            const baseStyle = {
78
+                marginRight: '8px'
79
+            };
80
+            
81
+            // 合并全局样式和item中的样式
82
+            const mergedStyle = {
83
+                ...baseStyle,
84
+                ...this.tagTextStyle,
85
+                ...(item.tagTextStyle || {})
86
+            };
87
+            
88
+            return mergedStyle;
89
+        }
90
+    }
91
+};
92
+</script>
93
+
94
+<style scoped>
95
+.text-tag-container {
96
+    display: flex;
97
+    flex-wrap: wrap;
98
+    gap: 8px;
99
+}
100
+
101
+.tag-item {
102
+    display: flex;
103
+    align-items: center;
104
+    padding: 4px 12px;
105
+    background-color: #f0f0f0;
106
+    border-radius: 16px;
107
+    font-size: 14px;
108
+}
109
+
110
+.tag-text {
111
+    margin-right: 8px;
112
+}
113
+</style>

+ 103 - 0
src/components/timeline-record/TimelineRecord.vue

@@ -0,0 +1,103 @@
1
+
2
+<template>
3
+  <view class="vertical-timeline">
4
+    <uni-section :title="sectionTitle" type="line" padding>
5
+      <view v-if="flatSteps.length > 0">
6
+        <uni-steps
7
+          :options="flatSteps"
8
+          :active="activeStep"
9
+          direction="column"
10
+        />
11
+      </view>
12
+      <view v-else class="no-record">
13
+        <uni-icons type="flag-filled" size="60" color="#ccc"></uni-icons>
14
+        <text>暂无打卡记录</text>
15
+      </view>
16
+    </uni-section>
17
+  </view>
18
+</template>
19
+
20
+<script>
21
+export default {
22
+  name: 'VerticalTimeline',
23
+  props: {
24
+    records: {
25
+      type: Array,
26
+      default: () => []
27
+    },
28
+    sectionTitle: {
29
+      type: String,
30
+      default: '打卡记录'
31
+    },
32
+    statusColorMap: {
33
+      type: Object,
34
+      default: () => ({
35
+        正常: '#67C23A',
36
+        迟到: '#F56C6C',
37
+        早退: '#E6A23C',
38
+        异常: '#909399'
39
+      })
40
+    }
41
+  },
42
+  computed: {
43
+    activeStep() {
44
+      return this.flatSteps.length - 1
45
+    },
46
+    flatSteps() {
47
+      if (!this.records || this.records.length === 0) return []
48
+      
49
+      // 提取items数组并排序
50
+      const items = this.records[0]?.items || []
51
+      const sorted = [...items].sort((a, b) => 
52
+        new Date(a.checkInTime) - new Date(b.checkInTime)
53
+      )
54
+
55
+      return sorted.map(item => ({
56
+        title: this.formatTime(item.checkInTime),
57
+        desc: `${this.getTypeText(item.checkInType)} - ${item.checkInPosition}`,
58
+        color: this.getStatusColor(item)
59
+      }))
60
+    }
61
+  },
62
+  methods: {
63
+    formatTime(timeStr) {
64
+      // return timeStr ? timeStr.split(' ')[1].substr(0,5) : '--:--'
65
+      return timeStr
66
+    },
67
+    getTypeText(type) {
68
+      const map = {
69
+        CLOCK_IN: '上班打卡',
70
+        CLOCK_OUT: '下班打卡'
71
+      }
72
+      return map[type] || type
73
+    },
74
+    getStatusColor(item) {
75
+      // 可根据实际业务逻辑判断状态
76
+      return this.statusColorMap.正常
77
+    }
78
+  }
79
+}
80
+</script>
81
+
82
+<style lang="scss" scoped>
83
+.vertical-timeline {
84
+  background-color: #fff;
85
+  border-radius: 12px;
86
+  padding: 10px 0;
87
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
88
+}
89
+
90
+.no-record {
91
+  display: flex;
92
+  flex-direction: column;
93
+  align-items: center;
94
+  justify-content: center;
95
+  padding: 40px 0;
96
+  color: #999;
97
+
98
+  text {
99
+    margin-top: 8px;
100
+    font-size: 14px;
101
+  }
102
+}
103
+</style>

+ 167 - 0
src/components/uni-section/uni-section.vue

@@ -0,0 +1,167 @@
1
+<template>
2
+	<view class="uni-section">
3
+		<view class="uni-section-header" @click="onClick">
4
+				<view class="uni-section-header__decoration" v-if="type" :class="type" />
5
+        <slot v-else name="decoration"></slot>
6
+
7
+        <view class="uni-section-header__content">
8
+          <text :style="{'font-size':titleFontSize,'color':titleColor}" class="uni-section__content-title" :class="{'distraction':!subTitle}">{{ title }}</text>
9
+          <text v-if="subTitle" :style="{'font-size':subTitleFontSize,'color':subTitleColor}" class="uni-section-header__content-sub">{{ subTitle }}</text>
10
+        </view>
11
+
12
+        <view class="uni-section-header__slot-right">
13
+          <slot name="right"></slot>
14
+        </view>
15
+		</view>
16
+
17
+		<view class="uni-section-content" :style="{padding: _padding}">
18
+			<slot />
19
+		</view>
20
+	</view>
21
+</template>
22
+
23
+<script>
24
+
25
+	/**
26
+	 * Section 标题栏
27
+	 * @description 标题栏
28
+	 * @property {String} type = [line|circle|square] 标题装饰类型
29
+	 * 	@value line 竖线
30
+	 * 	@value circle 圆形
31
+	 * 	@value square 正方形
32
+	 * @property {String} title 主标题
33
+	 * @property {String} titleFontSize 主标题字体大小
34
+	 * @property {String} titleColor 主标题字体颜色
35
+	 * @property {String} subTitle 副标题
36
+	 * @property {String} subTitleFontSize 副标题字体大小
37
+	 * @property {String} subTitleColor 副标题字体颜色
38
+	 * @property {String} padding 默认插槽 padding
39
+	 */
40
+
41
+	export default {
42
+		name: 'UniSection',
43
+    emits:['click'],
44
+		props: {
45
+			type: {
46
+				type: String,
47
+				default: ''
48
+			},
49
+			title: {
50
+				type: String,
51
+				required: true,
52
+				default: ''
53
+			},
54
+      titleFontSize: {
55
+        type: String,
56
+        default: '14px'
57
+      },
58
+			titleColor:{
59
+				type: String,
60
+				default: '#333'
61
+			},
62
+			subTitle: {
63
+				type: String,
64
+				default: ''
65
+			},
66
+      subTitleFontSize: {
67
+        type: String,
68
+        default: '12px'
69
+      },
70
+      subTitleColor: {
71
+        type: String,
72
+        default: '#999'
73
+      },
74
+			padding: {
75
+				type: [Boolean, String],
76
+				default: false
77
+			}
78
+		},
79
+    computed:{
80
+      _padding(){
81
+        if(typeof this.padding === 'string'){
82
+          return this.padding
83
+        }
84
+
85
+        return this.padding?'10px':''
86
+      }
87
+    },
88
+		watch: {
89
+			title(newVal) {
90
+				if (uni.report && newVal !== '') {
91
+					uni.report('title', newVal)
92
+				}
93
+			}
94
+		},
95
+    methods: {
96
+			onClick() {
97
+				this.$emit('click')
98
+			}
99
+		}
100
+	}
101
+</script>
102
+<style lang="scss" >
103
+	$uni-primary: #2979ff !default;
104
+
105
+	.uni-section {
106
+		background-color: #fff;
107
+    .uni-section-header {
108
+      position: relative;
109
+      /* #ifndef APP-NVUE */
110
+      display: flex;
111
+      /* #endif */
112
+      flex-direction: row;
113
+      align-items: center;
114
+      padding: 12px 10px;
115
+      font-weight: normal;
116
+
117
+      &__decoration{
118
+        margin-right: 6px;
119
+        background-color: $uni-primary;
120
+        &.line {
121
+          width: 4px;
122
+          height: 12px;
123
+          border-radius: 10px;
124
+        }
125
+
126
+        &.circle {
127
+          width: 8px;
128
+          height: 8px;
129
+          border-top-right-radius: 50px;
130
+          border-top-left-radius: 50px;
131
+          border-bottom-left-radius: 50px;
132
+          border-bottom-right-radius: 50px;
133
+        }
134
+
135
+        &.square {
136
+          width: 8px;
137
+          height: 8px;
138
+        }
139
+      }
140
+
141
+      &__content {
142
+        /* #ifndef APP-NVUE */
143
+        display: flex;
144
+        /* #endif */
145
+        flex-direction: column;
146
+        flex: 1;
147
+        color: #333;
148
+
149
+        .distraction {
150
+          flex-direction: row;
151
+          align-items: center;
152
+        }
153
+        &-sub {
154
+          margin-top: 2px;
155
+        }
156
+      }
157
+
158
+      &__slot-right{
159
+        font-size: 14px;
160
+      }
161
+    }
162
+
163
+    .uni-section-content{
164
+      font-size: 14px;
165
+    }
166
+	}
167
+</style>

+ 30 - 0
src/config.js

@@ -0,0 +1,30 @@
1
+// 应用全局配置
2
+module.exports = {
3
+   // 接口地址  本地调试使用 http://192.168.3.222:38080  打包使用 /prod-api
4
+ // baseUrl: process.env.NODE_ENV === 'development' ? 'http://192.168.3.221:82/prod-api' : '/prod-api', //生产
5
+  //  baseUrl: process.env.NODE_ENV === 'development' ? 'http://192.168.3.221:8080' : '/prod-api',
6
+// baseUrl: process.env.NODE_ENV === 'development' ? 'http://guangxi.qinghe.sundot.cn:8088' : 'http://guangxi.qinghe.sundot.cn:8088/prod-api',
7
+    baseUrl: 'http://airport.samsundot.com:9024/prod-api',//测试
8
+// baseUrl:'http://airport.samsundot.com:9021/prod-api',//生产
9
+  // 应用信息
10
+  appInfo: {
11
+    // 应用名称
12
+    name: "airport-app",
13
+    // 应用版本
14
+    version: "1.2.0",
15
+    // 应用logo
16
+    logo: "/static/logo.png",
17
+    // 官方网站
18
+    site_url: "http://ruoyi.vip",
19
+    // 政策协议
20
+    agreements: [{
21
+        title: "隐私政策",
22
+        url: "https://ruoyi.vip/protocol.html"
23
+      },
24
+      {
25
+        title: "用户服务协议",
26
+        url: "https://ruoyi.vip/protocol.html"
27
+      }
28
+    ]
29
+  }
30
+}

+ 23 - 0
src/main.js

@@ -0,0 +1,23 @@
1
+import Vue from 'vue'
2
+import App from './App'
3
+import store from './store' // store
4
+import plugins from './plugins' // plugins
5
+import './permission' // permission
6
+import { getDicts } from "@/api/system/dict/data"
7
+import uView from "uview-ui";
8
+
9
+Vue.use(uView);
10
+Vue.use(plugins)
11
+
12
+Vue.config.productionTip = false
13
+Vue.prototype.$store = store
14
+Vue.prototype.getDicts = getDicts
15
+
16
+App.mpType = 'app'
17
+//为了兼容echarts在移动端的点击事件
18
+window.wx={}
19
+const app = new Vue({
20
+  ...App
21
+})
22
+
23
+app.$mount()

+ 71 - 0
src/manifest.json

@@ -0,0 +1,71 @@
1
+{
2
+    "name" : "安检分级质控系统移动端",
3
+    "appid" : "__UNI__25A9D80",
4
+    "description" : "",
5
+    "versionName" : "1.2.0",
6
+    "versionCode" : "100",
7
+    "transformPx" : false,
8
+    "app-plus" : {
9
+        "usingComponents" : true,
10
+        "nvueCompiler" : "uni-app",
11
+        "splashscreen" : {
12
+            "alwaysShowBeforeRender" : true,
13
+            "waiting" : true,
14
+            "autoclose" : true,
15
+            "delay" : 0
16
+        },
17
+        "modules" : {},
18
+        "distribute" : {
19
+            "android" : {
20
+                "permissions" : [
21
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
22
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
23
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
24
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
25
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
26
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
27
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
28
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
29
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
30
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
31
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
32
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
33
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
34
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
35
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
36
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
37
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>"
38
+                ]
39
+            },
40
+            "ios" : {},
41
+            "sdkConfigs" : {}
42
+        }
43
+    },
44
+    "quickapp" : {},
45
+    "mp-weixin" : {
46
+        "appid" : "wxccd7e2a0911b3397",
47
+        "setting" : {
48
+            "urlCheck" : false,
49
+            "es6" : false,
50
+            "minified" : true,
51
+            "postcss" : true
52
+        },
53
+        "optimization" : {
54
+            "subPackages" : true
55
+        },
56
+        "usingComponents" : true
57
+    },
58
+    "vueVersion" : "2",
59
+    "h5" : {
60
+        "template" : "static/index.html",
61
+        "devServer" : {
62
+            "port" : 9090,
63
+            "https" : false
64
+        },
65
+        "title" : "Airport-App",
66
+        "router" : {
67
+            "mode" : "hash",
68
+            "base" : "./"
69
+        }
70
+    }
71
+}

+ 16 - 0
src/package.json

@@ -0,0 +1,16 @@
1
+{
2
+    "id": "text-li-switch",
3
+    "name": "switch文字开关,v-model绑定状态,支持参数改变文字样式",
4
+    "displayName": "switch文字开关,v-model绑定状态,支持参数改变文字样式",
5
+    "version": "1.0.2",
6
+    "description": "switch文字开关,v-model绑定状态,支持参数改变文字样式",
7
+    "keywords": [
8
+        "switch文字开关,v-model绑定状态,支持参数改变文字样式"
9
+    ],
10
+    "dcloudext": {
11
+        "category": [
12
+            "前端组件",
13
+            "通用组件"
14
+        ]
15
+    }
16
+}

+ 373 - 0
src/pages.json

@@ -0,0 +1,373 @@
1
+{
2
+  "pages": [
3
+    {
4
+      "path": "pages/login",
5
+      "style": {
6
+        "navigationBarTitleText": "登录"
7
+      }
8
+    },
9
+    {
10
+      "path": "pages/eikonStatistics/index",
11
+      "style": {
12
+        "navigationBarTitleText": "组织画像"
13
+      }
14
+    },
15
+    {
16
+      "path": "pages/register",
17
+      "style": {
18
+        "navigationBarTitleText": "注册"
19
+      }
20
+    },
21
+    {
22
+      "path": "pages/home/index",
23
+      "style": {
24
+        "navigationBarTitleText": "安检分级质控系统移动端",
25
+        "navigationStyle": "custom"
26
+      }
27
+    },
28
+    {
29
+      "path": "pages/home-new/index",
30
+      "style": {
31
+        "navigationBarTitleText": "安检分级质控系统移动端",
32
+        "navigationStyle": "custom"
33
+      }
34
+    },
35
+    {
36
+      "path": "pages/work/index",
37
+      "style": {
38
+        "navigationBarTitleText": "工作台"
39
+      }
40
+    },
41
+    {
42
+      "path": "pages/mine/index",
43
+      "style": {
44
+        "navigationBarTitleText": "我的"
45
+      }
46
+    },
47
+    {
48
+      "path": "pages/capabilityComparison/index",
49
+      "style": {
50
+        "navigationBarTitleText": "能力对比"
51
+      }
52
+    },
53
+    {
54
+      "path": "pages/statisticalReport/index",
55
+      "style": {
56
+        "navigationBarTitleText": "整体报表"
57
+      }
58
+    },
59
+    {
60
+      "path": "pages/mine/avatar/index",
61
+      "style": {
62
+        "navigationBarTitleText": "修改头像"
63
+      }
64
+    },
65
+    {
66
+      "path": "pages/mine/info/index",
67
+      "style": {
68
+        "navigationBarTitleText": "个人信息"
69
+      }
70
+    },
71
+    {
72
+      "path": "pages/mine/info/edit",
73
+      "style": {
74
+        "navigationBarTitleText": "编辑资料"
75
+      }
76
+    },
77
+    {
78
+      "path": "pages/mine/pwd/index",
79
+      "style": {
80
+        "navigationBarTitleText": "修改密码"
81
+      }
82
+    },
83
+    {
84
+      "path": "pages/mine/setting/index",
85
+      "style": {
86
+        "navigationBarTitleText": "应用设置"
87
+      }
88
+    },
89
+    {
90
+      "path": "pages/mine/help/index",
91
+      "style": {
92
+        "navigationBarTitleText": "常见问题"
93
+      }
94
+    },
95
+    {
96
+      "path": "pages/mine/about/index",
97
+      "style": {
98
+        "navigationBarTitleText": "关于我们"
99
+      }
100
+    },
101
+    {
102
+      "path": "pages/common/webview/index",
103
+      "style": {
104
+        "navigationBarTitleText": "浏览网页"
105
+      }
106
+    },
107
+    {
108
+      "path": "pages/common/textview/index",
109
+      "style": {
110
+        "navigationBarTitleText": "浏览文本"
111
+      }
112
+    },
113
+    {
114
+      "path": "pages/attendance/index",
115
+      "style": {
116
+        "navigationBarTitleText": "勤务打卡"
117
+      }
118
+    },
119
+    {
120
+      "path": "pages/attendanceStatistics/index",
121
+      "style": {
122
+        "navigationBarTitleText": "考勤统计"
123
+      }
124
+    },
125
+    {
126
+      "path": "pages/workProfile/index",
127
+      "style": {
128
+        "navigationBarTitleText": "工作画像"
129
+      }
130
+    },
131
+    {
132
+      "path": "pages/seizedReported/index",
133
+      "style": {
134
+        "navigationBarTitleText": "违禁品查获上报"
135
+      }
136
+    },
137
+    {
138
+      "path": "pages/seizedReportedVoice/index",
139
+      "style": {
140
+        "navigationBarTitleText": "违禁品查获上报"
141
+      }
142
+    },
143
+    {
144
+      "path": "pages/train/index",
145
+      "style": {
146
+        "navigationStyle": "custom"
147
+      }
148
+    },
149
+    {
150
+      "path": "pages/attendance/stats",
151
+      "style": {
152
+        "navigationBarTitleText": "打卡统计"
153
+      }
154
+    },
155
+    {
156
+      "path": "pages/attendance/stats",
157
+      "style": {
158
+        "navigationBarTitleText": "打卡统计"
159
+      }
160
+    },
161
+    {
162
+      "path": "pages/home/index",
163
+      "style": {
164
+        "navigationBarTitleText": "首页"
165
+      }
166
+    },
167
+    {
168
+      "path": "pages/home-new/index",
169
+      "style": {
170
+        "navigationBarTitleText": "首页"
171
+      }
172
+    },
173
+    {
174
+      "path": "pages/personal-center/index",
175
+      "style": {
176
+        "navigationBarTitleText": "个人中心"
177
+      }
178
+    },
179
+    {
180
+      "path": "pages/exam/index",
181
+      "style": {
182
+        "navigationBarTitleText": "测试"
183
+      }
184
+    },
185
+    {
186
+      "path": "pages/exam-list/index",
187
+      "style": {
188
+        "navigationBarTitleText": "测试列表"
189
+      }
190
+    },
191
+    {
192
+      "path": "pages/daily-exam/task-list/index",
193
+      "style": {
194
+        "navigationBarTitleText": "抽问抽答"
195
+      }
196
+    },
197
+    {
198
+      "path": "pages/daily-exam/answer/index",
199
+      "style": {
200
+        "navigationBarTitleText": "答题"
201
+      }
202
+    },
203
+    {
204
+      "path": "pages/inspectionRecord/index",
205
+      "style": {
206
+        "navigationBarTitleText": "巡检记录单"
207
+      }
208
+    },
209
+    {
210
+      "path": "pages/inspectionRecordList/index",
211
+      "style": {
212
+        "navigationBarTitleText": "巡检列表"
213
+      }
214
+    },
215
+    {
216
+      "path": "pages/checklist/index",
217
+      "style": {
218
+        "navigationBarTitleText": "检查单"
219
+      }
220
+    },
221
+    {
222
+      "path": "pages/announcement/index",
223
+      "style": {
224
+        "navigationBarTitleText": "公告"
225
+      }
226
+    },
227
+    {
228
+      "path": "pages/workDocu/index",
229
+      "style": {
230
+        "navigationBarTitleText": "工作手册"
231
+      }
232
+    },
233
+    {
234
+      "path": "pages/workDocu/workDocuDetail",
235
+      "style": {
236
+        "navigationBarTitleText": "工作手册详情"
237
+      }
238
+    },
239
+    {
240
+      "path": "pages/announcement/announcementDetail",
241
+      "style": {
242
+        "navigationBarTitleText": "公告详情"
243
+      }
244
+    },
245
+    {
246
+      "path": "pages/announcement/noticeDetail",
247
+      "style": {
248
+        "navigationBarTitleText": "通知详情"
249
+      }
250
+    },
251
+    {
252
+      "path": "pages/problemRect/index",
253
+      "style": {
254
+        "navigationBarTitleText": "问题记录及整改单"
255
+      }
256
+    },
257
+    {
258
+      "path": "pages/inspectionStatistics/index",
259
+      "style": {
260
+        "navigationBarTitleText": "巡检统计"
261
+      }
262
+    },
263
+    {
264
+      "path": "pages/messagePush/index",
265
+      "style": {
266
+        "navigationBarTitleText": "消息推送"
267
+      }
268
+    },
269
+    {
270
+      "path": "pages/qualityControlAnalysisReport/index",
271
+      "style": {
272
+        "navigationBarTitleText": "质控分析报告"
273
+      }
274
+    },
275
+    {
276
+      "path": "pages/questionStatistics/index",
277
+      "style": {
278
+        "navigationBarTitleText": "抽问抽答统计"
279
+      }
280
+    },
281
+    {
282
+      "path": "pages/myToDoList/index",
283
+      "style": {
284
+        "navigationBarTitleText": "我的事项"
285
+      }
286
+    },
287
+    {
288
+      "path": "pages/inspectionChecklist/index",
289
+      "style": {
290
+        "navigationBarTitleText": "巡视检查列表"
291
+      }
292
+    },
293
+    {
294
+      "path": "pages/voiceSubmissionDraft/index",
295
+      "style": {
296
+        "navigationBarTitleText": "草稿箱"
297
+      }
298
+    },
299
+    {
300
+      "path": "pages/inspectionRecord/detail",
301
+      "style": {
302
+        "navigationBarTitleText": "巡检单明细"
303
+      }
304
+    },
305
+    {
306
+      "path": "pages/seizureRecord/detail",
307
+      "style": {
308
+        "navigationBarTitleText": "查获明细"
309
+      }
310
+    },
311
+    {
312
+      "path": "pages/statisticalAnalysis/index",
313
+      "style": {
314
+        "navigationBarTitleText": "统计分析"
315
+      }
316
+    },
317
+    {
318
+      "path": "pages/seizureRecord/index",
319
+      "style": {
320
+        "navigationBarTitleText": "查获记录"
321
+      }
322
+    },
323
+    {
324
+      "path": "pages/seizeStatistics/index",
325
+      "style": {
326
+        "navigationBarTitleText": "查获统计"
327
+      }
328
+    }
329
+  ],
330
+  "tabBar": {
331
+    "custom": true,
332
+    "color": "#000000",
333
+    "selectedColor": "#000000",
334
+    "borderStyle": "white",
335
+    "backgroundColor": "#ffffff",
336
+    "paddingBottom": "0px",
337
+    "marginBottom": "0px",
338
+    "list": [
339
+      {
340
+        "pagePath": "pages/home-new/index",
341
+        "iconPath": "static/images/tabbar/home.png",
342
+        "selectedIconPath": "static/images/tabbar/home_.png",
343
+        "text": "首页"
344
+      },
345
+      {
346
+        "pagePath": "pages/myToDoList/index",
347
+        "iconPath": "/static/images/tabbar/message.png",
348
+        "selectedIconPath": "/static/images/tabbar/message_.png",
349
+        "text": "消息"
350
+      },
351
+      {
352
+        "pagePath": "pages/work/index",
353
+        "iconPath": "static/images/tabbar/work.png",
354
+        "selectedIconPath": "static/images/tabbar/work_.png",
355
+        "text": "工作台"
356
+      },
357
+      {
358
+        "pagePath": "pages/mine/index",
359
+        "iconPath": "static/images/tabbar/mine.png",
360
+        "selectedIconPath": "static/images/tabbar/mine_.png",
361
+        "text": "我的"
362
+      }
363
+    ]
364
+  },
365
+  "easycom": {
366
+    "^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
367
+  },
368
+  "globalStyle": {
369
+    "navigationBarTextStyle": "black",
370
+    "navigationBarTitleText": "Airport",
371
+    "navigationBarBackgroundColor": "#FFFFFF"
372
+  }
373
+}

+ 182 - 0
src/pages/announcement/announcementDetail.vue

@@ -0,0 +1,182 @@
1
+<template>
2
+    <home-container :customStyle="{ background: 'none' }" :customHomeStyle="{ overflow: 'hidden' }">
3
+        <view class="detail-container" v-if="detailData">
4
+            <!-- 公告标题 -->
5
+            <view class="title">{{ detailData.noticeTitle }}</view>
6
+            
7
+            <!-- 发布信息 -->
8
+            <view class="meta-info">
9
+                <text class="author">{{ detailData.createUser || '管理员' }}</text>
10
+                <text class="date">{{ formatDate(detailData.createTime) }}</text>
11
+            </view>
12
+            
13
+            <!-- 分隔线 -->
14
+            <view class="divider"></view>
15
+            
16
+            <!-- 富文本内容 -->
17
+            <view class="content" v-html="detailData.noticeContent"></view>
18
+        </view>
19
+        
20
+        <!-- 加载状态 -->
21
+        <view v-else class="loading">加载中...</view>
22
+    </home-container>
23
+</template>
24
+
25
+<script>
26
+import HomeContainer from "@/components/HomeContainer.vue";
27
+import { getNoticeDetail } from "@/api/announcement/announcement.js";
28
+
29
+
30
+export default {
31
+    components: {
32
+        HomeContainer
33
+    },
34
+    data() {
35
+        return {
36
+            detailData: null,
37
+            loading: true
38
+        };
39
+    },
40
+    onLoad(options) {
41
+        
42
+        // 从路由参数中获取公告ID
43
+        if (options && options.id) {
44
+            this.getDetailData(options.id);
45
+        } else {
46
+            console.error('公告ID不存在');
47
+            uni.showToast({
48
+                title: '获取公告详情失败',
49
+                icon: 'none'
50
+            });
51
+        }
52
+    },
53
+    methods: {
54
+        // 格式化日期
55
+        formatDate(timeString) {
56
+            if (!timeString) return '';
57
+            const date = new Date(timeString);
58
+            const year = date.getFullYear();
59
+            const month = String(date.getMonth() + 1).padStart(2, '0');
60
+            const day = String(date.getDate()).padStart(2, '0');
61
+            const hour = String(date.getHours()).padStart(2, '0');
62
+            const minute = String(date.getMinutes()).padStart(2, '0');
63
+            return `${year}-${month}-${day} ${hour}:${minute}`;
64
+        },
65
+        
66
+        // 处理富文本中的图片
67
+        handleRichTextImages(html) {
68
+            if (!html) return '';
69
+            
70
+            // 为所有img标签添加style="width:90%",同时保留原有的style属性
71
+            return html.replace(/<img([^>]+)>/g, (match, attributes) => {
72
+                // 检查是否已有style属性
73
+                if (attributes.includes('style=')) {
74
+                    // 如果已有style属性,在其末尾添加width设置
75
+                    return `<img${attributes.replace(/style=["']([^"']*)["']/, (styleMatch, styleValue) => {
76
+                        return `style="${styleValue} width:90%;"`;
77
+                    })}>`;
78
+                } else {
79
+                    // 如果没有style属性,添加一个包含width设置的style属性
80
+                    return `<img${attributes} style="width:90%;">`;
81
+                }
82
+            });
83
+        },
84
+
85
+        // 获取公告详情数据
86
+        async getDetailData(id) {
87
+            try {
88
+                this.loading = true;
89
+                const response = await getNoticeDetail(id);
90
+                
91
+                // 深拷贝响应数据
92
+                this.detailData = JSON.parse(JSON.stringify(response.data));
93
+                
94
+                // 处理noticeContent中的图片样式
95
+                if (this.detailData.noticeContent) {
96
+                    this.detailData.noticeContent = this.handleRichTextImages(this.detailData.noticeContent);
97
+                }
98
+            } catch (error) {
99
+                console.error('获取公告详情失败:', error);
100
+                uni.showToast({
101
+                    title: '获取公告详情失败',
102
+                    icon: 'none'
103
+                });
104
+            } finally {
105
+                this.loading = false;
106
+            }
107
+        }
108
+    }
109
+};
110
+</script>
111
+
112
+<style lang="scss" scoped>
113
+.detail-container {
114
+    padding: 32rpx;
115
+    background-color: #ffffff;
116
+    min-height: calc(100vh - 177rpx);
117
+    box-sizing: border-box;
118
+    
119
+    // 标题样式
120
+    .title {
121
+        font-size: 36rpx;
122
+        font-weight: bold;
123
+        color: #333333;
124
+        line-height: 52rpx;
125
+        margin-bottom: 24rpx;
126
+    }
127
+    
128
+    // 元信息样式
129
+    .meta-info {
130
+        display: flex;
131
+        justify-content: space-between;
132
+        align-items: center;
133
+        margin-bottom: 24rpx;
134
+        
135
+        .author,
136
+        .date {
137
+            font-size: 28rpx;
138
+            color: #666666;
139
+        }
140
+    }
141
+    
142
+    // 分隔线样式
143
+    .divider {
144
+        height: 1rpx;
145
+        background-color: #e5e5e5;
146
+        margin: 32rpx 0;
147
+    }
148
+    
149
+    // 富文本内容样式
150
+    .content {
151
+        font-size: 28rpx;
152
+        color: #333333;
153
+        line-height: 48rpx;
154
+        
155
+        // 富文本内部样式
156
+        :deep() {
157
+            p {
158
+                margin-bottom: 20rpx;
159
+            }
160
+            img {
161
+                max-width: 100%;
162
+                height: auto;
163
+                margin: 16rpx 0;
164
+            }
165
+            a {
166
+                color: #007aff;
167
+                text-decoration: underline;
168
+            }
169
+        }
170
+    }
171
+}
172
+
173
+// 加载状态样式
174
+.loading {
175
+    display: flex;
176
+    justify-content: center;
177
+    align-items: center;
178
+    height: 400rpx;
179
+    font-size: 28rpx;
180
+    color: #999999;
181
+}
182
+</style>

+ 204 - 0
src/pages/announcement/index.vue

@@ -0,0 +1,204 @@
1
+<template>
2
+    <home-container :customStyle="{ background: 'none' }" :customHomeStyle="{ overflow: 'hidden' }">
3
+        <view class="announcement-list">
4
+            <scroll-view scroll-y="true" @refresherrefresh="onRefresh" refresher-enabled
5
+                :refresher-triggered="refresherTriggered" @scrolltolower="loadMore" :style="{ height: '100%' }"
6
+                enable-back-to-top="true">
7
+                <list-card v-for="item in list" :key="item.id" @click="navigateToDetail(item)">
8
+                    <template #title>
9
+                        <view class="list-title">
10
+                            {{ item.noticeTitle }}
11
+                        </view>
12
+                    </template>
13
+                    <template #default>
14
+                        <view class="list-info">
15
+                            <view class="list-meta">
16
+                                <view class="author">{{ item.createUser || '管理员' }}</view>
17
+                                <view class="date">{{ formatDate(item.createTime) }}</view>
18
+                            </view>
19
+                            <view class="content">
20
+                                {{ stripHtmlTags(item.noticeContent) }}
21
+                            </view>
22
+                        </view>
23
+                    </template>
24
+                </list-card>
25
+
26
+                <!-- 加载状态提示 -->
27
+                <view v-if="loading" class="load-more">
28
+                    <text>加载中...</text>
29
+                </view>
30
+                <view v-else-if="!hasMore && list.length > 0" class="no-more">
31
+                    <text>没有更多数据了</text>
32
+                </view>
33
+                <view v-else-if="list.length === 0" class="no-data">
34
+                    <text>暂无公告</text>
35
+                </view>
36
+            </scroll-view>
37
+        </view>
38
+    </home-container>
39
+</template>
40
+
41
+<script>
42
+import HomeContainer from "@/components/HomeContainer.vue";
43
+import ListCard from "@/components/list-card/list-card.vue";
44
+import { getNoticeList } from "@/api/announcement/announcement.js";
45
+
46
+export default {
47
+    components: {
48
+        HomeContainer,
49
+        ListCard
50
+    },
51
+    data() {
52
+        return {
53
+            list: [],
54
+            // 分页参数
55
+            pageNum: 1,
56
+            pageSize: 10,
57
+            total: 0,
58
+            loading: false,
59
+            hasMore: true,
60
+            // scroll-view下拉刷新状态控制
61
+            refresherTriggered: false
62
+        }
63
+    },
64
+    methods: {
65
+        // 格式化日期
66
+        formatDate(timeString) {
67
+            if (!timeString) return '';
68
+            const date = new Date(timeString);
69
+            const year = date.getFullYear();
70
+            const month = String(date.getMonth() + 1).padStart(2, '0');
71
+            const day = String(date.getDate()).padStart(2, '0');
72
+            return `${year}-${month}-${day}`;
73
+        },
74
+
75
+        // 去除HTML标签,只保留纯文本
76
+        stripHtmlTags(html) {
77
+            if (!html) return '';
78
+            // 使用正则表达式去除HTML标签
79
+            return html.replace(/<[^>]+>/g, '');
80
+        },
81
+        // 跳转到详情页(如果有的话)
82
+        navigateToDetail(item) {
83
+            // 这里可以根据需要跳转到公告详情页
84
+            // 目前先打印日志
85
+            uni.navigateTo({
86
+                url: '/pages/announcement/announcementDetail?id=' + item.noticeId
87
+            });
88
+
89
+        },
90
+        onRefresh() {
91
+            this.pageNum = 1;
92
+            this.refresherTriggered = true;
93
+            this.loadData();
94
+        },
95
+        // 加载数据方法
96
+        async loadData() {
97
+            if (this.loading) return;
98
+
99
+            this.loading = true;
100
+            try {
101
+                const query = {
102
+                    pageNum: this.pageNum,
103
+                    pageSize: this.pageSize,
104
+                    noticeType: 2, // 根据之前的组件,这里固定为2
105
+                    status: 0
106
+                };
107
+
108
+                let response = await getNoticeList(query);
109
+
110
+                // 处理响应数据
111
+                const data = response.rows || response.list || [];
112
+
113
+                if (this.pageNum === 1) {
114
+                    this.list = data;
115
+                } else {
116
+                    this.list = [...this.list, ...data];
117
+                }
118
+
119
+                this.total = response.total || 0;
120
+                this.hasMore = this.list.length < this.total;
121
+            } catch (error) {
122
+                console.error('加载公告数据失败:', error);
123
+                uni.showToast({
124
+                    title: '加载失败',
125
+                    icon: 'none'
126
+                });
127
+            } finally {
128
+                this.loading = false;
129
+                // 重置下拉刷新状态
130
+                this.refresherTriggered = false;
131
+                uni.stopPullDownRefresh();
132
+            }
133
+        },
134
+        // 加载更多数据
135
+        loadMore() {
136
+            if (this.loading || !this.hasMore) return;
137
+            this.pageNum++;
138
+            this.loadData();
139
+        }
140
+    },
141
+    mounted() {
142
+        this.loadData();
143
+    },
144
+    onShow() {
145
+        // 页面再次展示时刷新数据
146
+        this.pageNum = 1;
147
+        this.loadData();
148
+    }
149
+}
150
+</script>
151
+
152
+<style lang="scss" scoped>
153
+.announcement-list {
154
+    height: calc(100vh - 177rpx);
155
+    margin-bottom: 20rpx;
156
+
157
+    // 标题样式
158
+    .list-title {
159
+        font-size: 32rpx;
160
+        font-weight: bold;
161
+        color: #333333;
162
+    }
163
+
164
+    // 信息样式
165
+    .list-info {
166
+        .list-meta {
167
+            display: flex;
168
+            justify-content: flex-start;
169
+            margin-bottom: 11px;
170
+            font-size: 16px;
171
+            color: #666666;
172
+            align-items: center;
173
+
174
+            .author {
175
+                margin-right: 30rpx;
176
+            }
177
+        }
178
+
179
+        .content {
180
+            font-size: 28rpx;
181
+            color: #666666;
182
+            line-height: 42rpx;
183
+            word-break: break-all;
184
+            display: -webkit-box;
185
+            -webkit-line-clamp: 2;
186
+            -webkit-box-orient: vertical;
187
+            overflow: hidden;
188
+            text-overflow: ellipsis;
189
+        }
190
+    }
191
+
192
+    /* 加载状态样式 */
193
+    .load-more,
194
+    .no-more,
195
+    .no-data {
196
+        display: flex;
197
+        justify-content: center;
198
+        align-items: center;
199
+        padding: 40rpx;
200
+        font-size: 28rpx;
201
+        color: #999;
202
+    }
203
+}
204
+</style>

+ 182 - 0
src/pages/announcement/noticeDetail.vue

@@ -0,0 +1,182 @@
1
+<template>
2
+    <home-container :customStyle="{ background: 'none' }" :customHomeStyle="{ overflow: 'hidden' }">
3
+        <view class="detail-container" v-if="detailData">
4
+            <!-- 公告标题 -->
5
+            <view class="title">{{ detailData.noticeTitle }}</view>
6
+            
7
+            <!-- 发布信息 -->
8
+            <view class="meta-info">
9
+                <text class="author">{{ detailData.createUser || '管理员' }}</text>
10
+                <text class="date">{{ formatDate(detailData.createTime) }}</text>
11
+            </view>
12
+            
13
+            <!-- 分隔线 -->
14
+            <view class="divider"></view>
15
+            
16
+            <!-- 富文本内容 -->
17
+            <view class="content" v-html="detailData.noticeContent"></view>
18
+        </view>
19
+        
20
+        <!-- 加载状态 -->
21
+        <view v-else class="loading">加载中...</view>
22
+    </home-container>
23
+</template>
24
+
25
+<script>
26
+import HomeContainer from "@/components/HomeContainer.vue";
27
+import { getNoticeDetail } from "@/api/announcement/announcement.js";
28
+
29
+
30
+export default {
31
+    components: {
32
+        HomeContainer
33
+    },
34
+    data() {
35
+        return {
36
+            detailData: null,
37
+            loading: true
38
+        };
39
+    },
40
+    onLoad(options) {
41
+        
42
+        // 从路由参数中获取公告ID
43
+        if (options && options.id) {
44
+            this.getDetailData(options.id);
45
+        } else {
46
+            console.error('公告ID不存在');
47
+            uni.showToast({
48
+                title: '获取公告详情失败',
49
+                icon: 'none'
50
+            });
51
+        }
52
+    },
53
+    methods: {
54
+        // 格式化日期
55
+        formatDate(timeString) {
56
+            if (!timeString) return '';
57
+            const date = new Date(timeString);
58
+            const year = date.getFullYear();
59
+            const month = String(date.getMonth() + 1).padStart(2, '0');
60
+            const day = String(date.getDate()).padStart(2, '0');
61
+            const hour = String(date.getHours()).padStart(2, '0');
62
+            const minute = String(date.getMinutes()).padStart(2, '0');
63
+            return `${year}-${month}-${day} ${hour}:${minute}`;
64
+        },
65
+        
66
+        // 处理富文本中的图片
67
+        handleRichTextImages(html) {
68
+            if (!html) return '';
69
+            
70
+            // 为所有img标签添加style="width:90%",同时保留原有的style属性
71
+            return html.replace(/<img([^>]+)>/g, (match, attributes) => {
72
+                // 检查是否已有style属性
73
+                if (attributes.includes('style=')) {
74
+                    // 如果已有style属性,在其末尾添加width设置
75
+                    return `<img${attributes.replace(/style=["']([^"']*)["']/, (styleMatch, styleValue) => {
76
+                        return `style="${styleValue} width:90%;"`;
77
+                    })}>`;
78
+                } else {
79
+                    // 如果没有style属性,添加一个包含width设置的style属性
80
+                    return `<img${attributes} style="width:90%;">`;
81
+                }
82
+            });
83
+        },
84
+
85
+        // 获取公告详情数据
86
+        async getDetailData(id) {
87
+            try {
88
+                this.loading = true;
89
+                const response = await getNoticeDetail(id);
90
+                
91
+                // 深拷贝响应数据
92
+                this.detailData = JSON.parse(JSON.stringify(response.data));
93
+                
94
+                // 处理noticeContent中的图片样式
95
+                if (this.detailData.noticeContent) {
96
+                    this.detailData.noticeContent = this.handleRichTextImages(this.detailData.noticeContent);
97
+                }
98
+            } catch (error) {
99
+                console.error('获取公告详情失败:', error);
100
+                uni.showToast({
101
+                    title: '获取公告详情失败',
102
+                    icon: 'none'
103
+                });
104
+            } finally {
105
+                this.loading = false;
106
+            }
107
+        }
108
+    }
109
+};
110
+</script>
111
+
112
+<style lang="scss" scoped>
113
+.detail-container {
114
+    padding: 32rpx;
115
+    background-color: #ffffff;
116
+    min-height: calc(100vh - 177rpx);
117
+    box-sizing: border-box;
118
+    
119
+    // 标题样式
120
+    .title {
121
+        font-size: 36rpx;
122
+        font-weight: bold;
123
+        color: #333333;
124
+        line-height: 52rpx;
125
+        margin-bottom: 24rpx;
126
+    }
127
+    
128
+    // 元信息样式
129
+    .meta-info {
130
+        display: flex;
131
+        justify-content: space-between;
132
+        align-items: center;
133
+        margin-bottom: 24rpx;
134
+        
135
+        .author,
136
+        .date {
137
+            font-size: 28rpx;
138
+            color: #666666;
139
+        }
140
+    }
141
+    
142
+    // 分隔线样式
143
+    .divider {
144
+        height: 1rpx;
145
+        background-color: #e5e5e5;
146
+        margin: 32rpx 0;
147
+    }
148
+    
149
+    // 富文本内容样式
150
+    .content {
151
+        font-size: 28rpx;
152
+        color: #333333;
153
+        line-height: 48rpx;
154
+        
155
+        // 富文本内部样式
156
+        :deep() {
157
+            p {
158
+                margin-bottom: 20rpx;
159
+            }
160
+            img {
161
+                max-width: 100%;
162
+                height: auto;
163
+                margin: 16rpx 0;
164
+            }
165
+            a {
166
+                color: #007aff;
167
+                text-decoration: underline;
168
+            }
169
+        }
170
+    }
171
+}
172
+
173
+// 加载状态样式
174
+.loading {
175
+    display: flex;
176
+    justify-content: center;
177
+    align-items: center;
178
+    height: 400rpx;
179
+    font-size: 28rpx;
180
+    color: #999999;
181
+}
182
+</style>

+ 733 - 0
src/pages/attendance/components/AddAttendancePersonnelModal.vue

@@ -0,0 +1,733 @@
1
+<template>
2
+  <view class="addAttendancePersonnelModal">
3
+    <view class="slot-content" @click.stop="openModal">
4
+      <slot></slot>
5
+    </view>
6
+    <u-popup :show="show" mode="center" :round="8">
7
+      <view class="modal-content">
8
+        <view class="title">
9
+          <text>{{ notkezhang ? '当班人员' : '负责区域' }}</text>
10
+          <u-icon name="close" color="#666666" size="20" @click="close" />
11
+        </view>
12
+        <view class="title-cell" v-if="notkezhang">
13
+          <SelectData ref="selectData" @search="searchUsers" @searchViewShowEvent="searchViewShowHandler" />
14
+        </view>
15
+        <view class="title-cell" v-if="!searchViewShow && selectedUser && selectedUser.length">
16
+          <view class="title-text">{{ `班组成员(${selectedUser.length}人)` }}</view>
17
+          <view class="personnel-list">
18
+            <view class="personnel-item" v-for="item of selectedUser" :key="item.userId"
19
+              @click="removeSelcetUser(item)">
20
+              <view class="personnel-img">
21
+                <UserAvatar :userName="item.nickName || item.userName" :avatarLink="item.avatar" />
22
+              </view>
23
+              <view class="personnel-name">{{ item.nickName || item.userName }}</view>
24
+              <view class="personnel-close" v-if="notkezhang">
25
+                <u-icon name="close" color="#666666" size="14" />
26
+              </view>
27
+            </view>
28
+          </view>
29
+        </view>
30
+        <view class="title-cell" v-if="notkezhang && searchViewShow">
31
+          <view style="margin-bottom: 15px;" v-if="addWorkUsers && addWorkUsers.length">
32
+            <view class="title-text">
33
+              {{ `添加人员(${addWorkUsers.length}人)` }}
34
+            </view>
35
+            <view class="personnel-list" v-if="addWorkUsers && addWorkUsers.length">
36
+              <view class="personnel-item" v-for="item of addWorkUsers" :key="item.userId" @click="selectAddUser(item)">
37
+                <view class="personnel-img">
38
+                  <UserAvatar :userName="item.nickName" :avatarLink="item.avatar" />
39
+                </view>
40
+                <view class="personnel-name">{{ item.nickName }}</view>
41
+                <view class="personnel-close">
42
+                  <u-icon name="close" color="#666666" size="14" />
43
+                </view>
44
+              </view>
45
+            </view>
46
+          </view>
47
+          <view class="title-text">
48
+            {{ `搜索结果(${allUsers.length}人)` }}
49
+          </view>
50
+          <view class="personnel-list" v-if="allUsers && allUsers.length">
51
+            <view class="personnel-item" v-for="item of allUsers" :key="item.userId" @click="selectAddUser(item)">
52
+              <view class="personnel-img">
53
+                <UserAvatar :userName="item.nickName" :avatarLink="item.avatar" />
54
+              </view>
55
+              <view class="personnel-name">{{ item.nickName }}</view>
56
+            </view>
57
+          </view>
58
+          <view v-else class="empty"></view>
59
+        </view>
60
+        <view class="title-cell" v-if="!searchViewShow">
61
+          <view class="title-text">{{ notkezhang ? '选择上通道时间' : '选择上区域时间' }}</view>
62
+          <uni-datetime-picker type="datetime" v-model="currentTime" />
63
+        </view>
64
+        <view class="title-cell" v-if="!searchViewShow">
65
+          <view class="title-text" style="display: flex; align-items: center;">{{ notkezhang ? '选择通道' : '选择区域' }}
66
+            <view v-if="!userInfo.roles.includes('banzuzhang')">
67
+              <SelectArea :notkezhang="notkezhang" @selected="selectedArea">
68
+                <u-icon name="plus-circle" color="#2A70D1" size="25" />
69
+              </SelectArea>
70
+            </view>
71
+          </view>
72
+          <view class="">
73
+            <uniDataPicker v-if="userInfo.roles.includes('banzuzhang')" :localdata="channelList"
74
+              :popup-title="notkezhang ? '请选择上岗通道' : '请选择上岗区域'" v-model="channelOrRegional"
75
+              @change="onLocationChange" />
76
+
77
+            <view class="selected-areas" v-if="selectedAreas && selectedAreas.length">
78
+              <view class="area-item" v-for="area in selectedAreas" :key="area.id">
79
+                <view class="area-label">{{ area.label }}<u-icon name="close" color="#666666" size="14"
80
+                    @click="removeArea(area)" /></view>
81
+                <text-tag :tags="area.children" @remove="removeSubArea($event, area.id)" />
82
+              </view>
83
+            </view>
84
+          </view>
85
+        </view>
86
+
87
+        <view class="footer-btn">
88
+          <view v-if="searchViewShow" class="custom-btn-normal" @click="appendWorkUsers"
89
+            :class="{ disabled: addWorkUsers.length === 0 }">添加人员</view>
90
+          <view v-else class="custom-btn-normal" style="margin-top: 10rpx;" @click="invokerSubmitPostDuty"
91
+            :class="{ disabled: selectedUserIds.length === 0 }">{{ notkezhang ? '确认上通道' : '确认上区域' }}</view>
92
+          <u-modal :show="errModalShow" @confirm="errModalShow = false" width="80vw">
93
+            <view class="modal-slot-content">
94
+              {{ errModalContent }}
95
+            </view>
96
+          </u-modal>
97
+        </view>
98
+      </view>
99
+    </u-popup>
100
+  </view>
101
+</template>
102
+
103
+<script>
104
+import SelectData from './SelectData'
105
+import SelectArea from './SelectArea'
106
+import TimePicker from './TimePicker'
107
+import {
108
+  getUserList,
109
+  addPostRecord,
110
+  getPostRecordList,
111
+  dataConfigTree,
112
+  memberList,
113
+  queryLastTime
114
+} from "@/api/attendance/attendance"
115
+import moment from 'moment'
116
+import uniDataPicker from '@/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker'
117
+import { formatTime, formatName, isAfterTodayStart } from '@/utils/formatUtils'
118
+import { generateRandomDigits } from '@/utils/handler'
119
+import UserAvatar from './UserAvatar'
120
+import { getHandleAreaData } from '@/utils/common'
121
+export default {
122
+  components: { uniDataPicker, SelectData, TimePicker, UserAvatar, SelectArea },
123
+  props: {
124
+    userInfo: {
125
+      type: Object,
126
+      default: () => ({})
127
+    },
128
+    disabled: {
129
+      type: Boolean,
130
+      default: false
131
+    },
132
+    notkezhang: {
133
+      type: Boolean,
134
+      default: undefined
135
+    },
136
+    selectedMember: {
137
+      type: Array,
138
+      default: () => []
139
+    },
140
+    attendanceInfo: { // 考勤信息
141
+      type: Object,
142
+      default: () => ({})
143
+    },
144
+  },
145
+  data() {
146
+    return {
147
+      show: false,
148
+      searchFocus: false,
149
+      channelOrRegional: '',
150
+      channelOrRegionalName: '',
151
+      channelList: [
152
+        { value: 0, text: "通道A" },
153
+      ],
154
+      selectedUser: [], //选中的人员
155
+      selectedAreas: [], // 选中的区域
156
+      allUsers: [],
157
+      addWorkUsers: [], // 额外添加的人员 
158
+      teamUsers: [],
159
+      searchViewShow: false,
160
+      isSubmittingPost: false,
161
+      currentTime: undefined,
162
+      errModalShow: false,
163
+      errModalContent: ''
164
+    }
165
+  },
166
+  computed: {
167
+    selectedUserIds() {
168
+      return (this.selectedUser || []).map(item => item.userId)
169
+    },
170
+    //单选获取的区域
171
+    positions() {
172
+      return this.notkezhang ? {
173
+        regionalCode: '',
174
+        regionalName: '',
175
+        channelCode: this.channelOrRegional,
176
+        channelName: this.channelOrRegionalName
177
+      } : {
178
+        regionalCode: this.channelOrRegional,
179
+        regionalName: this.channelOrRegionalName,
180
+        channelCode: '',
181
+        channelName: ''
182
+      }
183
+    },
184
+    //多选获得的区域
185
+    getSelectArea() {
186
+      let area = []
187
+      this.selectedAreas.forEach(item => {
188
+        item.children.forEach(child => {
189
+          area.push(this.notkezhang ? {
190
+            regionalCode: '',
191
+            regionalName: '',
192
+            channelCode: child.code,
193
+            channelName: child.label,
194
+            terminlCode: item.code,
195
+            terminlName: item.label
196
+          } : {
197
+            regionalCode: child.code,
198
+            regionalName: child.label,
199
+            channelCode: '',
200
+            channelName: '',
201
+            terminlCode: item.code,
202
+            terminlName: item.label
203
+          })
204
+        })
205
+      })
206
+      return area
207
+    }
208
+  },
209
+  watch: {
210
+    show(newValue) {
211
+      if (newValue) {
212
+        if (this.userInfo.roles.includes('kezhang')) {
213
+          this.getQueryLastTime()
214
+        }
215
+        this.currentTime = formatTime(new Date())
216
+
217
+        if (this.notkezhang) {
218
+          if (this.userInfo.roles.includes('banzuzhang')) {
219
+            memberList({
220
+              attendanceTeamId: this.userInfo.teamsId,
221
+              attendanceDate: moment().format('YYYY-MM-DD')
222
+            }).then(res => {
223
+              console.log(res.rows, "res.rows")
224
+              this.selectedUser = res.data || []
225
+
226
+            })
227
+            return
228
+          }
229
+          this.loadTeamUsers().then(() => {
230
+            // 选中全组人员  
231
+            this.teamUsers.forEach((item) => {
232
+              this.selectUserHandler(item, false)
233
+            })
234
+          })
235
+
236
+        } else {
237
+          this.loadTeamUsers().then(() => {
238
+            // 查自己 
239
+            const curUser = this.teamUsers.find((item) => {
240
+              return item.userId === this.userInfo.userId
241
+            })
242
+            this.selectUserHandler(curUser, false)
243
+          })
244
+
245
+        }
246
+      } else {
247
+        this.allUsers = []
248
+      }
249
+    },
250
+    notkezhang: {
251
+      handler(newValue) {
252
+        const level = newValue ? 3 : 2
253
+        this.invokerDataConfigTree(level)
254
+      },
255
+      immediate: true
256
+    }
257
+  },
258
+  methods: {
259
+    //查询最后一次上岗的区域
260
+    async getQueryLastTime() {
261
+      let res = await queryLastTime({
262
+        userId: this.userInfo.userId,
263
+      })
264
+      this.selectedAreas = getHandleAreaData(res.data || [], this.notkezhang)
265
+      console.log(this.selectedAreas, "this.selectedAreas")
266
+    },
267
+    convertTree(list = []) {
268
+      return list.map(node => ({
269
+        text: node.label,
270
+        value: node.code,
271
+        children: node.children ? this.convertTree(node.children) : null
272
+      }))
273
+    },
274
+    invokerDataConfigTree(level) {
275
+      return dataConfigTree(level).then(res => {
276
+        this.channelList = this.convertTree(res.data || [])
277
+      }).catch(() => {
278
+        this.channelList = []
279
+      })
280
+    },
281
+    onLocationChange({ detail }) {
282
+      const { value } = detail
283
+      this.channelOrRegionalName = value.map(item => item.text).join('/')
284
+    },
285
+    openModal() {
286
+      if (this.disabled || (this.selectedMember.length === 0 && this.notkezhang)) return
287
+      if ((!this.attendanceInfo.checkInTime || (this.attendanceInfo.checkInTime && this.attendanceInfo.checkOutTime))) {
288
+        return;
289
+      }
290
+      this.show = true
291
+    },
292
+    close() {
293
+      this.show = false
294
+    },
295
+    //获取多选的区域
296
+    selectedArea(areas) {
297
+
298
+      let res = areas.filter(area => {
299
+        return area.children.some((item) => item.checked)
300
+      })
301
+      res = res.map(item => {
302
+        return {
303
+          ...item,
304
+          children: item.children.filter(child => child.checked)
305
+        }
306
+      })
307
+      this.selectedAreas = res
308
+    },
309
+    removeArea(area) {
310
+      this.selectedAreas = this.selectedAreas.filter(a => a.id !== area.id)
311
+    },
312
+    removeSubArea(tags, id) {
313
+      console.log(tags, id)
314
+      // debugger
315
+      const area = this.selectedAreas.find(a => a.id === id)
316
+      area.children = tags
317
+
318
+    },
319
+    searchViewShowHandler(show) { // 是否展示搜索用户界面
320
+      this.searchViewShow = show || this.addWorkUsers.length
321
+    },
322
+    selectAddUser(selectItem) { // 移除额外添加人员
323
+      const index = this.addWorkUsers.findIndex(item => item.userId === selectItem.userId)
324
+      if (index >= 0) {
325
+        this.addWorkUsers.splice(index, 1)
326
+      } else {
327
+        this.addWorkUsers.push(selectItem)
328
+      }
329
+    },
330
+    appendWorkUsers() {
331
+      if (this.addWorkUsers.length) {
332
+        this.addWorkUsers.forEach(item => {
333
+          this.selectUserHandler(item, false)
334
+        })
335
+        this.addWorkUsers = []
336
+        this.searchViewShow = false
337
+        this.$refs.selectData.clearInput()
338
+      }
339
+    },
340
+    removeSelcetUser(selectItem, onlyCheck = false) {
341
+      const index = this.selectedUser.findIndex(item => item.userId === selectItem.userId)
342
+      if (index >= 0) {
343
+        !onlyCheck && this.selectedUser.splice(index, 1)
344
+        return undefined
345
+      }
346
+      return selectItem
347
+    },
348
+    checkUserStatus(selectItem) {
349
+      return getPostRecordList({
350
+        userId: selectItem.userId,
351
+        pageNum: 1,
352
+        pageSize: 1
353
+      }).then(res => {
354
+        if (Array(res.rows) && res.rows.length && res.rows[0].checkOutTime === '2000-01-01 00:00:00') {
355
+          if (isAfterTodayStart(res.rows[0].checkInTime)) {
356
+            return `${selectItem.nickName || selectItem.userName} 尚未下通道;`
357
+          }
358
+        } else {
359
+          return ''
360
+        }
361
+      })
362
+    },
363
+    selectUserHandler(selectItem, unique = true) {
364
+      // 🔥 重要:先检查用户是否可以上通道(最新记录的checkOutTime必须不是默认时间)
365
+      const addItem = this.removeSelcetUser(selectItem, !unique)
366
+
367
+      if (addItem) {
368
+        this.selectedUser.push(addItem)
369
+      }
370
+    },
371
+    invokerSubmitPostDuty() {
372
+      if (this.selectedUser.length === 0) {
373
+        uni.showToast({
374
+          title: '请选择至少一位人员',
375
+          icon: 'none'
376
+        });
377
+        return;
378
+      }
379
+      if ((!this.notkezhang && this.getSelectArea.length == 0) || (this.notkezhang && !this.channelOrRegional)) {
380
+        uni.showToast({
381
+          title: this.notkezhang ? '请选择上岗通道' : '请选择上岗区域',
382
+          icon: 'none'
383
+        });
384
+        return;
385
+      }
386
+      debugger
387
+      const checkUserStatusALL = this.selectedUser.map(item => {
388
+        return this.checkUserStatus(item)
389
+      })
390
+      uni.showLoading({ title: '正在查询人员状态...' });
391
+      debugger
392
+      Promise.all(checkUserStatusALL).then((res) => {
393
+        const checkResult = res.filter(Boolean)
394
+        if (checkResult.length) {
395
+          this.errModalContent = checkResult.join('\n')
396
+          this.errModalShow = true
397
+        } else {
398
+          this.submitPostDuty()
399
+        }
400
+      }).finally(() => {
401
+        uni.hideLoading()
402
+      })
403
+    },
404
+    // 修改:提交上通道记录到后端,checkOutTime设为2000年1月1日0点
405
+    submitPostDuty() {
406
+      if (this.isSubmittingPost) return;
407
+      this.isSubmittingPost = true;
408
+      uni.showLoading({ title: '正在上通道...' });
409
+      const currentTime = this.currentTime
410
+      // 获取当前用户信息
411
+      const currentUserInfo = this.userInfo;
412
+      // 创建随机工作组id
413
+      const groupId = generateRandomDigits()
414
+      // 为每个选中的用户创建上通道记录
415
+      const promises = this.selectedUser.map((item) => {
416
+        let postRecordList = []
417
+        //如果是科长就是走多选逻辑,如果是班组长就走单选逻辑
418
+        if (!this.notkezhang) {
419
+          postRecordList = this.getSelectArea.map(ele => {
420
+            return {
421
+              userId: item.userId,
422
+              userName: item.nickName || item.userName,
423
+              checkInTime: formatTime(currentTime, 'YYYY-MM-DD hh:mm:ss'),
424
+              // 🔥 重要:上通道时传入默认下岗时间(2000年1月1日0点)
425
+              checkOutTime: '2000-01-01 00:00:00',
426
+              attendanceDate: formatTime(currentTime, 'YYYY-MM-DD'),
427
+              // 🔥 使用从getInfo获取的完整用户信息
428
+              attendanceTeamId: currentUserInfo.teamsId || currentUserInfo.deptId,
429
+              attendanceTeamName: currentUserInfo.teamsName || '未知班组',
430
+              attendanceDepartmentId: currentUserInfo.departmentId,
431
+              attendanceDepartmentName: currentUserInfo.departmentName || '未知部门',
432
+              attendanceStationId: currentUserInfo.stationId,
433
+              attendanceStationName: currentUserInfo.stationName || '机场',
434
+              remark: '手动添加上通道记录',
435
+              terminlCode: '',
436
+              terminlName: '',
437
+              positionCode: '',
438
+              positionName: '',
439
+              shiftCode: groupId,
440
+              shiftName: '',
441
+              ...ele
442
+            }
443
+          })
444
+        } else {
445
+          //如果是班组长走单选逻辑
446
+          postRecordList = [{
447
+            userId: item.userId,
448
+            userName: item.nickName || item.userName,
449
+            checkInTime: formatTime(currentTime, 'YYYY-MM-DD hh:mm:ss'),
450
+            // 🔥 重要:上通道时传入默认下岗时间(2000年1月1日0点)
451
+            checkOutTime: '2000-01-01 00:00:00',
452
+            attendanceDate: formatTime(currentTime, 'YYYY-MM-DD'),
453
+            // 🔥 使用从getInfo获取的完整用户信息
454
+            attendanceTeamId: currentUserInfo.teamsId || currentUserInfo.deptId,
455
+            attendanceTeamName: currentUserInfo.teamsName || '未知班组',
456
+            attendanceDepartmentId: currentUserInfo.departmentId,
457
+            attendanceDepartmentName: currentUserInfo.departmentName || '未知部门',
458
+            attendanceStationId: currentUserInfo.stationId,
459
+            attendanceStationName: currentUserInfo.stationName || '机场',
460
+            remark: '手动添加上通道记录',
461
+            terminlCode: '',
462
+            terminlName: '',
463
+            positionCode: '',
464
+            positionName: '',
465
+            shiftCode: groupId,
466
+            shiftName: '',
467
+            ...this.positions
468
+          }];
469
+        }
470
+        console.log(postRecordList, "postRecordList")
471
+        debugger
472
+
473
+        return addPostRecord(postRecordList)
474
+      });
475
+
476
+      Promise.all(promises).then((results) => {
477
+        // 检查是否有失败的记录
478
+        const failedCount = results.filter(result => !result || result.code !== 200).length;
479
+        const successCount = this.selectedUser.length - failedCount;
480
+
481
+        if (successCount > 0) {
482
+          uni.showToast({
483
+            title: `成功上通道${successCount}人${failedCount > 0 ? `,${failedCount}人失败` : ''}`,
484
+            icon: successCount === this.selectedUser.length ? 'success' : 'none',
485
+            duration: 2000
486
+          });
487
+
488
+          // 清空已添加的用户列表
489
+          this.selectedUser = [];
490
+          // 重置通道信息
491
+          this.channelOrRegional = '',
492
+            this.channelOrRegionalName = '',
493
+
494
+            this.$emit('updateRecord', () => {
495
+              this.show = false
496
+            })
497
+        }
498
+      }).catch((error) => {
499
+        uni.showToast({
500
+          title: '上通道失败:请稍后重试',
501
+          icon: 'error',
502
+          duration: 2000
503
+        });
504
+      }).finally(() => {
505
+        uni.hideLoading();
506
+        this.isSubmittingPost = false;
507
+      })
508
+    },
509
+
510
+    //搜索用户
511
+    async searchUsers(value) {
512
+      const keyword = value.trim();
513
+      if (!keyword) {
514
+        this.allUsers = [];
515
+        return;
516
+      }
517
+      try {
518
+        // 调用用户搜索接口,不限制部门
519
+        const response = await getUserList({
520
+          nickName: keyword,    // 按昵称搜索
521
+          status: '0'          // 只获取正常状态的用户
522
+        });
523
+        if (response && response.code === 200) {
524
+          this.allUsers = (response.rows || []).map(item => {
525
+            return {
526
+              ...item,
527
+              nickName: formatName(item.nickName)
528
+            }
529
+          });
530
+        } else {
531
+          this.allUsers = [];
532
+        }
533
+      } catch (error) {
534
+        this.allUsers = [];
535
+        uni.showToast({
536
+          title: '搜索失败,请重试',
537
+          icon: 'none',
538
+          duration: 2000
539
+        });
540
+      }
541
+    },
542
+
543
+    // 加载同组用户
544
+    async loadTeamUsers() {
545
+      try {
546
+        const currentUserInfo = this.userInfo || {}
547
+        const currentUserDeptId = currentUserInfo.deptId || currentUserInfo.teamsId;
548
+        // 调用用户列表接口,传递deptId获取同组用户
549
+        const response = await getUserList({
550
+          deptId: currentUserDeptId, // 🔥 使用从getInfo获取的deptId
551
+          status: '0' // 只获取正常状态的用户
552
+        });
553
+
554
+        if (response && response.code === 200) {
555
+          this.teamUsers = (response.rows || []).map(item => {
556
+            return {
557
+              ...item,
558
+              nickName: formatName(item.nickName)
559
+            }
560
+          })
561
+        } else {
562
+          throw new Error(response.msg || '获取同组用户失败');
563
+        }
564
+      } catch (error) {
565
+        uni.showToast({
566
+          title: '加载同组用户失败',
567
+          icon: 'none',
568
+          duration: 2000
569
+        });
570
+        // 设置空数组,避免页面报错
571
+        this.teamUsers = [];
572
+      }
573
+    },
574
+  },
575
+}
576
+</script>
577
+
578
+<style lang="scss" scoped>
579
+.addAttendancePersonnelModal {
580
+  .slot-content {}
581
+
582
+  .empty {
583
+    margin: 0 auto;
584
+    width: 160px;
585
+    height: 155px;
586
+    background: url("../../../static/images/Empty.png") no-repeat;
587
+    background-size: cover;
588
+  }
589
+
590
+  .modal-content {
591
+    width: 90vw;
592
+    max-height: 75vh;
593
+    padding: 10px 0;
594
+
595
+    .title {
596
+      height: 24px;
597
+      font-weight: 400;
598
+      font-size: 18px;
599
+      color: #333333;
600
+      line-height: 21px;
601
+      display: flex;
602
+      justify-content: space-between;
603
+      align-items: center;
604
+      padding: 15px;
605
+      box-sizing: border-box;
606
+    }
607
+
608
+    .title-cell {
609
+      padding: 10px 15px;
610
+      box-sizing: border-box;
611
+
612
+      .selected-areas {
613
+        .area-item {
614
+          .area-label {
615
+            height: 65rpx;
616
+            line-height: 65rpx;
617
+            font-weight: 400;
618
+            font-size: 18px;
619
+            display: flex;
620
+            align-items: center;
621
+          }
622
+        }
623
+      }
624
+
625
+      .title-text {
626
+        height: 16px;
627
+        font-weight: 400;
628
+        font-size: 14px;
629
+        color: #333333;
630
+        line-height: 16px;
631
+        text-align: left;
632
+        margin-bottom: 14px;
633
+
634
+      }
635
+    }
636
+
637
+    .personnel-list {
638
+      display: flex;
639
+      flex-wrap: wrap;
640
+      row-gap: 8px;
641
+      column-gap: 6px;
642
+
643
+      .personnel-item {
644
+        width: fit-content;
645
+        height: 34px;
646
+        background: #F0F0F0;
647
+        border-radius: 6px;
648
+        display: flex;
649
+        align-items: center;
650
+        column-gap: 6px;
651
+        padding: 0 5px;
652
+
653
+        .personnel-img {
654
+          width: 24px;
655
+          height: 24px;
656
+          border-radius: 6px;
657
+          overflow: hidden;
658
+        }
659
+
660
+        .personnel-name {
661
+          width: fit-content;
662
+          font-weight: 400;
663
+          font-size: 13px;
664
+          color: #3D3D3D;
665
+          text-align: left;
666
+          font-style: normal;
667
+          text-transform: none;
668
+        }
669
+
670
+        .personnel-close {
671
+          width: 16px;
672
+        }
673
+
674
+        .radio-button {
675
+          width: 14px;
676
+          height: 14px;
677
+          border: 1px solid #2196F3;
678
+          background: #fff;
679
+          border-radius: 50%;
680
+          position: relative;
681
+          transition: border-color 0.2s ease;
682
+        }
683
+
684
+        /* 激活状态 - 蓝色边框 */
685
+        .radio-button.active {
686
+          border-color: #496CF4;
687
+        }
688
+
689
+        /* 激活状态 - 中间的蓝点 */
690
+        .radio-button.active::after {
691
+          content: '';
692
+          position: absolute;
693
+          top: 50%;
694
+          left: 50%;
695
+          transform: translate(-50%, -50%);
696
+          width: 7px;
697
+          height: 7px;
698
+          background-color: #2196F3;
699
+          border-radius: 50%;
700
+        }
701
+
702
+        /* 聚焦状态 */
703
+        .radio-button:focus {
704
+          outline: none;
705
+          box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.3);
706
+        }
707
+
708
+        /* 悬停效果 */
709
+        .radio-button:hover .radio-button:not(.active) {
710
+          border-color: #999;
711
+        }
712
+
713
+        /* 禁用状态 */
714
+        .radio-button.disabled {
715
+          opacity: 0.5;
716
+          border-color: #999;
717
+          background: #f0f0f0;
718
+          cursor: not-allowed;
719
+        }
720
+      }
721
+    }
722
+
723
+    .footer-btn {
724
+      padding: 10px 15px;
725
+
726
+      .modal-slot-content {
727
+        white-space: pre;
728
+
729
+      }
730
+    }
731
+  }
732
+}
733
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 290 - 0
src/pages/attendance/components/AttendanceControl.vue


+ 594 - 0
src/pages/attendance/components/MaintainAreaOrMemberModal.vue

@@ -0,0 +1,594 @@
1
+<template>
2
+    <view class="teamMemberModal">
3
+        <view class="slot-content">
4
+            <view class="title">工作区域/班组成员</view>
5
+            <view class="content" v-if="selectedMember.length > 0">
6
+                <view class="title-cell">
7
+                    <view class="title-text">工作区域</view>
8
+                    <view class="terminal-list">
9
+                        {{ selectedMember[0].terminlName || '航站楼' }}
10
+                    </view>
11
+                </view>
12
+                <view class="member-cell" v-if="selectedMember && selectedMember.length">
13
+                    <view class="title-text">班组成员</view>
14
+                    <view class="personnel-list">
15
+                        <view class="personnel-item" v-for="item of selectedMember" :key="item.userId">
16
+                            <view class="personnel-img">
17
+                                <UserAvatar :userName="item.userName" :avatarLink="item.avatar" />
18
+                            </view>
19
+                            <view class="personnel-name">{{ item.userName }}</view>
20
+                        </view>
21
+                    </view>
22
+                </view>
23
+            </view>
24
+            <view v-else :class="{ 'custom-btn': true, 'disabled': (!attendanceInfo.checkInTime||(attendanceInfo.checkInTime && attendanceInfo.checkOutTime)) }" @click="openModal">
25
+                维护工作区域/班组成员</view>
26
+        </view>
27
+        <u-popup :show="show" mode="center" :round="8">
28
+            <view class="modal-content">
29
+                <view class="title">
30
+                    <text>班组成员</text>
31
+                    <u-icon name="close" color="#666666" size="20" @click="close" />
32
+                </view>
33
+
34
+                <view class="search-box">
35
+                    <SelectData ref="selectData" @search="searchUsers" @searchViewShowEvent="searchViewShowHandler" />
36
+                </view>
37
+
38
+                <view class="title-cell" v-if="searchViewShow">
39
+                    <view style="margin-bottom: 15px;" v-if="addWorkUsers && addWorkUsers.length">
40
+                        <view class="title-text">
41
+                            {{ `添加人员(${addWorkUsers.length}人)` }}
42
+                        </view>
43
+                        <view class="personnel-list" v-if="addWorkUsers && addWorkUsers.length">
44
+                            <view class="personnel-item" v-for="item of addWorkUsers" :key="item.userId"
45
+                                @click="selectAddUser(item)">
46
+                                <view class="personnel-img">
47
+                                    <UserAvatar :userName="item.nickName" :avatarLink="item.avatar" />
48
+                                </view>
49
+                                <view class="personnel-name">{{ item.nickName }}</view>
50
+                                <view class="personnel-close">
51
+                                    <u-icon name="close" color="#666666" size="14" />
52
+                                </view>
53
+                            </view>
54
+                        </view>
55
+                    </view>
56
+                    <view class="title-text">
57
+                        {{ `搜索结果(${allUsers.length}人)` }}
58
+                    </view>
59
+                    <view class="personnel-list" v-if="allUsers && allUsers.length">
60
+                        <view class="personnel-item" v-for="item of allUsers" :key="item.userId"
61
+                            @click="selectAddUser(item)">
62
+                            <view class="personnel-img">
63
+                                <UserAvatar :userName="item.nickName" :avatarLink="item.avatar" />
64
+                            </view>
65
+                            <view class="personnel-name">{{ item.nickName }}</view>
66
+                        </view>
67
+                    </view>
68
+                    <view v-else class="empty"></view>
69
+                </view>
70
+
71
+                <view class="title-cell" v-if="!searchViewShow && teamMembers && teamMembers.length">
72
+                    <view class="title-text">{{ `班组成员(${teamMembers.length}人)` }}</view>
73
+                    <view class="personnel-list">
74
+                        <view class="personnel-item" v-for="item of teamMembers" :key="item.userId">
75
+                            <view class="personnel-img">
76
+                                <UserAvatar :userName="item.nickName" :avatarLink="item.avatar" />
77
+                            </view>
78
+                            <view class="personnel-name">{{ item.nickName }}</view>
79
+                            <view class="personnel-close" @click="removeMember(item)">
80
+                                <u-icon name="close" color="#666666" size="14" />
81
+                            </view>
82
+                        </view>
83
+                    </view>
84
+                </view>
85
+
86
+                <view class="title-cell" v-if="!searchViewShow">
87
+                    <view class="title-text">工作区域</view>
88
+                    <view class="terminal-list">
89
+                        <view class="terminal-item" v-for="terminal in channelList" :key="terminal.value"
90
+                            :class="{ active: selectedTerminal === terminal.value }"
91
+                            @click="selectTerminal(terminal.value)">
92
+                            {{ terminal.text }}
93
+                        </view>
94
+                    </view>
95
+                </view>
96
+
97
+                <view class="footer-btn">
98
+                    <view v-if="searchViewShow" class="custom-btn-normal" @click="appendWorkUsers"
99
+                        :class="{ disabled: addWorkUsers.length === 0 }">
100
+                        <text>添加人员</text>
101
+                    </view>
102
+                    <view v-else class="custom-btn-normal" @click="confirm"
103
+                        :class="{ disabled: !selectedTerminal || loading }">
104
+                        <text v-if="loading">处理中...</text>
105
+                        <text v-else>确认</text>
106
+                    </view>
107
+                </view>
108
+            </view>
109
+        </u-popup>
110
+    </view>
111
+</template>
112
+
113
+<script>
114
+import UserAvatar from './UserAvatar'
115
+import { dataConfigTree, memberList, addMember, getUserList } from "@/api/attendance/attendance"
116
+import { formatName } from '@/utils/formatUtils'
117
+import SelectData from './SelectData'
118
+import moment from 'moment'
119
+export default {
120
+    components: { UserAvatar, SelectData },
121
+    props: {
122
+        userInfo: {
123
+            type: Object,
124
+            default: () => { }
125
+        },
126
+        attendanceInfo: {
127
+            type: Object,
128
+            default: () => ({
129
+                attendanceDate: '', // 当前日期
130
+                checkInTime: '', //上班打卡时间
131
+                checkOutTime: '', //下班打卡时间
132
+                items: [], // 打开记录 第一条为最早上班打卡时间 最后一条为最晚下班打卡时间
133
+            })
134
+        },
135
+    },
136
+    data() {
137
+        return {
138
+            show: false,
139
+            searchText: '',
140
+            selectedTerminal: '',
141
+            teamMembers: [],//新增里面显示的member
142
+            channelList: [],
143
+            selectedMember: [],//已选择班组数据
144
+
145
+            userOptions: [],
146
+            allUsers: [],//所有的人员
147
+            searchViewShow: false,//搜索人员
148
+            addWorkUsers: [], // 额外添加的人员 
149
+            teamUsers: [],//同组用户,
150
+            loading: false // 防止重复点击的loading状态
151
+        }
152
+    },
153
+    computed: {
154
+        notkezhang() {
155
+            // console.log(!this.userInfo.roles || !this.userInfo.roles.includes('kezhang'), "!this.userInfo.roles || !this.userInfo.roles.includes('kezhang')")
156
+            return !this.userInfo.roles || !this.userInfo.roles.includes('kezhang')
157
+        },
158
+    },
159
+    mounted() {
160
+        this.invokerDataConfigTree(3)
161
+        console.log(this.userInfo, "userInfo")
162
+
163
+        //获取选中的成员
164
+        this.getSelectedMember()
165
+    },
166
+    methods: {
167
+        selectAddUser(selectItem) { // 移除额外添加人员
168
+            console.log(selectItem, "selectItem", this.addWorkUsers)
169
+
170
+            const index = this.addWorkUsers.findIndex(item => item.userId === selectItem.userId)
171
+            if (index >= 0) {
172
+                this.addWorkUsers.splice(index, 1)
173
+            } else {
174
+                this.addWorkUsers.push(selectItem)
175
+            }
176
+        },
177
+        appendWorkUsers() {
178
+            console.log("appendWorkUsers", this.addWorkUsers, this.teamMembers)
179
+            if (this.addWorkUsers.length) {
180
+                this.addWorkUsers.forEach(item => {
181
+                    if (!this.teamMembers.some(member => member.userId === item.userId)) {
182
+                        this.teamMembers.push(item)
183
+                    }
184
+                })
185
+                this.addWorkUsers = []
186
+                this.searchViewShow = false
187
+                this.$refs.selectData.clearInput()
188
+            }
189
+        },
190
+        searchViewShowHandler(show) { // 是否展示搜索用户界面
191
+
192
+            if (this.addWorkUsers.length) {
193
+                this.searchViewShow = show || this.addWorkUsers.length
194
+            } else {
195
+                this.searchViewShow = true
196
+            }
197
+        },
198
+        //获取默认的teammember
199
+        getTeamMember() {
200
+
201
+            this.loadTeamUsers().then(() => {
202
+                // 选中全组人员  
203
+                this.teamMembers = this.teamUsers
204
+            })
205
+        },
206
+        // 加载同组用户
207
+        async loadTeamUsers() {
208
+            try {
209
+                const currentUserInfo = this.userInfo || {}
210
+                const currentUserDeptId = currentUserInfo.deptId || currentUserInfo.teamsId;
211
+                // 调用用户列表接口,传递deptId获取同组用户
212
+                const response = await getUserList({
213
+                    deptId: currentUserDeptId, // 🔥 使用从getInfo获取的deptId
214
+                    status: '0', // 只获取正常状态的用户
215
+                    pageSize: 100
216
+                });
217
+
218
+                if (response && response.code === 200) {
219
+                    this.teamUsers = (response.rows || []).map(item => {
220
+                        return {
221
+                            ...item,
222
+                            nickName: formatName(item.nickName)
223
+                        }
224
+                    })
225
+                } else {
226
+                    throw new Error(response.msg || '获取同组用户失败');
227
+                }
228
+            } catch (error) {
229
+                uni.showToast({
230
+                    title: '加载同组用户失败',
231
+                    icon: 'none',
232
+                    duration: 2000
233
+                });
234
+                // 设置空数组,避免页面报错
235
+                this.teamUsers = [];
236
+            }
237
+        },
238
+        //回显member
239
+        getSelectedMember() {
240
+
241
+            memberList({
242
+                attendanceTeamId: this.userInfo.teamsId,
243
+                attendanceDate: moment().format('YYYY-MM-DD')
244
+            }).then(res => {
245
+
246
+                this.selectedMember = res.data || []
247
+                this.$emit('update-member', this.selectedMember)
248
+            })
249
+
250
+
251
+
252
+        },
253
+        //新增时候显示的member
254
+        async initMember() {
255
+            const currentUserInfo = this.userInfo || {}
256
+            const currentUserDeptId = currentUserInfo.deptId || currentUserInfo.teamsId;
257
+            // 调用用户列表接口,传递deptId获取同组用户
258
+            const response = await getUserList({
259
+                deptId: currentUserDeptId, // 🔥 使用从getInfo获取的deptId
260
+                status: '0', // 只获取正常状态的用户
261
+                pageSize: 100
262
+            });
263
+            console.log(response, "response")
264
+            if (response && response.code === 200) {
265
+                this.userOptions = (response.rows || []).map(item => ({
266
+                    ...item,
267
+                    value: item.userId,
268
+                    text: formatName(item.nickName)
269
+                }));
270
+            } else {
271
+                throw new Error(response.msg || '获取同组用户失败');
272
+            }
273
+        },
274
+        handleUserSelect(selected) {
275
+            this.teamMembers = this.userOptions
276
+                .filter(option => selected.map(item => item.value).includes(option.value))
277
+                .map(option => ({
278
+                    ...option,
279
+                    userId: option.value,
280
+                    nickName: option.text,
281
+                    avatar: ''
282
+                }));
283
+        },
284
+        convertTree(list = []) {
285
+            return list.map(node => ({
286
+                text: node.label,
287
+                value: node.code,
288
+                children: node.children ? this.convertTree(node.children) : null
289
+            }))
290
+        },
291
+        invokerDataConfigTree(level) {
292
+            return dataConfigTree(level).then(res => {
293
+                this.channelList = this.convertTree(res.data || [])
294
+                console.log(this.channelList, "this.channelList")
295
+            }).catch(() => {
296
+                this.channelList = []
297
+            })
298
+        },
299
+        //搜索用户
300
+        async searchUsers(value) {
301
+
302
+            const keyword = value.trim();
303
+            if (!keyword) {
304
+                this.allUsers = [];
305
+                return;
306
+            }
307
+            try {
308
+                // 调用用户搜索接口,不限制部门
309
+                const response = await getUserList({
310
+                    nickName: keyword,    // 按昵称搜索
311
+                    status: '0', // 只获取正常状态的用户
312
+                    pageSize: 100        // 只获取正常状态的用户
313
+                });
314
+                if (response && response.code === 200) {
315
+                    this.allUsers = (response.rows || []).map(item => {
316
+                        return {
317
+                            ...item,
318
+                            nickName: formatName(item.nickName)
319
+                        }
320
+                    });
321
+                } else {
322
+                    this.allUsers = [];
323
+                }
324
+            } catch (error) {
325
+                this.allUsers = [];
326
+                uni.showToast({
327
+                    title: '搜索失败,请重试',
328
+                    icon: 'none',
329
+                    duration: 2000
330
+                });
331
+            }
332
+        },
333
+        close() {
334
+            if(this.searchViewShow){
335
+                this.searchViewShow = false;
336
+                return;
337
+            }
338
+
339
+            this.show = false
340
+        },
341
+        removeMember(member) {
342
+            this.teamMembers = this.teamMembers.filter(item => item.userId !== member.userId)
343
+
344
+        },
345
+        selectTerminal(terminal) {
346
+            this.selectedTerminal = terminal
347
+        },
348
+        confirm() {
349
+            if (!this.selectedTerminal) return;
350
+            this.loading = true;
351
+            let selectTerminal = this.channelList.find(item => item.value === this.selectedTerminal)
352
+            let res = this.teamMembers.map(item => {
353
+                return {
354
+                    ...item,
355
+                    nickName: item.nickName.replace(/\s+/g, ""),
356
+                    attendanceTeamId: this.userInfo.teamsId,
357
+                    attendanceTeamName: this.userInfo.teamsName,
358
+                    terminlCode: selectTerminal.value,
359
+                    terminlName: selectTerminal.text,
360
+                    userName: item.nickName.replace(/\s+/g, ""),
361
+                    userId: item.userId,
362
+                }
363
+            })
364
+
365
+            addMember(res).then(res => {
366
+                uni.showToast({ title: '提交成功', icon: 'success' });
367
+                this.close()
368
+                this.getSelectedMember()
369
+                return res;
370
+            }).finally(() => {
371
+                this.loading = false
372
+            })
373
+        },
374
+        openModal() {
375
+            if ((!this.attendanceInfo.checkInTime||(this.attendanceInfo.checkInTime && this.attendanceInfo.checkOutTime))) {
376
+                return;
377
+            }
378
+            this.show = true;
379
+            this.initMember()
380
+            this.getTeamMember()
381
+        }
382
+    }
383
+}
384
+</script>
385
+
386
+<style lang="scss" scoped>
387
+.teamMemberModal {
388
+    .slot-content {
389
+
390
+        width: 100%;
391
+        background: #FFFFFF;
392
+        box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.08);
393
+        border-radius: 16px 16px 16px 16px;
394
+        border: 1px solid #F0F8FF;
395
+        padding: 15px;
396
+        box-sizing: border-box;
397
+
398
+        .title {
399
+            height: 48rpx;
400
+            font-weight: 400;
401
+            font-size: 33rpx;
402
+            color: #333333;
403
+            line-height: 42rpx;
404
+            display: flex;
405
+            justify-content: space-between;
406
+            align-items: center;
407
+            // padding: 30rpx;
408
+            box-sizing: border-box;
409
+        }
410
+
411
+        .content {
412
+            // padding: 0 46rpx;
413
+
414
+
415
+            .title-cell {
416
+                display: flex;
417
+                height: 36px;
418
+                line-height: 36px;
419
+                justify-content: space-between;
420
+                color: #222222;
421
+                font-size: 27rpx;
422
+                // padding: 10rpx 0;
423
+
424
+                .terminal-list {
425
+                    color: #6C6C6C;
426
+                }
427
+            }
428
+
429
+            .member-cell {
430
+                display: flex;
431
+
432
+
433
+                color: #222222;
434
+                font-size: 27rpx;
435
+
436
+                flex-direction: column;
437
+
438
+                .title-text {
439
+                    height: 36px;
440
+                    line-height: 36px;
441
+                }
442
+
443
+                .personnel-list {
444
+                    display: flex;
445
+                    flex-wrap: wrap;
446
+                    row-gap: 19rpx;
447
+                    column-gap: 19rpx;
448
+
449
+                    .personnel-item {
450
+                        width: fit-content;
451
+                        height: 68rpx;
452
+                        background: #F0F0F0;
453
+                        border-radius: 12rpx;
454
+                        display: flex;
455
+                        align-items: center;
456
+                        column-gap: 12rpx;
457
+                        padding: 0 10rpx;
458
+
459
+                        .personnel-img {
460
+                            width: 48rpx;
461
+                            height: 48rpx;
462
+                            border-radius: 12rpx;
463
+                            overflow: hidden;
464
+                        }
465
+
466
+                        .personnel-name {
467
+                            width: fit-content;
468
+                            font-weight: 400;
469
+                            font-size: 26rpx;
470
+                            color: #3D3D3D;
471
+                            text-align: left;
472
+                            font-style: normal;
473
+                            text-transform: none;
474
+                        }
475
+
476
+                        .personnel-close {
477
+                            width: 32rpx;
478
+                        }
479
+                    }
480
+                }
481
+            }
482
+        }
483
+
484
+    }
485
+
486
+    .modal-content {
487
+        width: 90vw;
488
+        max-height: 75vh;
489
+        padding: 20rpx 0;
490
+
491
+        .title {
492
+            height: 48rpx;
493
+            font-weight: 400;
494
+            font-size: 36rpx;
495
+            color: #333333;
496
+            line-height: 42rpx;
497
+            display: flex;
498
+            justify-content: space-between;
499
+            align-items: center;
500
+            padding: 30rpx;
501
+            box-sizing: border-box;
502
+        }
503
+
504
+        .search-box {
505
+            padding: 20rpx 30rpx;
506
+        }
507
+
508
+        .title-cell {
509
+            padding: 20rpx 30rpx;
510
+            box-sizing: border-box;
511
+
512
+            .title-text {
513
+                height: 32rpx;
514
+                font-weight: 400;
515
+                font-size: 28rpx;
516
+                color: #333333;
517
+                line-height: 32rpx;
518
+                text-align: left;
519
+                margin-bottom: 28rpx;
520
+                text-indent: 10rpx;
521
+            }
522
+        }
523
+
524
+        .personnel-list {
525
+            display: flex;
526
+            flex-wrap: wrap;
527
+            row-gap: 16rpx;
528
+            column-gap: 12rpx;
529
+
530
+            .personnel-item {
531
+                width: fit-content;
532
+                height: 68rpx;
533
+                background: #F0F0F0;
534
+                border-radius: 12rpx;
535
+                display: flex;
536
+                align-items: center;
537
+                column-gap: 12rpx;
538
+                padding: 0 10rpx;
539
+
540
+                .personnel-img {
541
+                    width: 48rpx;
542
+                    height: 48rpx;
543
+                    border-radius: 12rpx;
544
+                    overflow: hidden;
545
+                }
546
+
547
+                .personnel-name {
548
+                    width: fit-content;
549
+                    font-weight: 400;
550
+                    font-size: 26rpx;
551
+                    color: #3D3D3D;
552
+                    text-align: left;
553
+                    font-style: normal;
554
+                    text-transform: none;
555
+                }
556
+
557
+                .personnel-close {
558
+                    width: 32rpx;
559
+                }
560
+            }
561
+        }
562
+
563
+        .terminal-list {
564
+            display: flex;
565
+            flex-direction: row;
566
+            gap: 20rpx;
567
+
568
+            .terminal-item {
569
+                padding: 20rpx;
570
+                border: 1px solid #ddd;
571
+                border-radius: 12rpx;
572
+                text-align: center;
573
+                cursor: pointer;
574
+                transition: all 0.3s;
575
+
576
+                &.active {
577
+                    background-color: #f0f0f0;
578
+                    border-color: #2196F3;
579
+                }
580
+
581
+                &:hover {
582
+                    background-color: #f5f5f5;
583
+                }
584
+            }
585
+        }
586
+
587
+        .footer-btn {
588
+            padding: 20rpx 30rpx;
589
+
590
+
591
+        }
592
+    }
593
+}
594
+</style>

+ 275 - 0
src/pages/attendance/components/SearchView.vue

@@ -0,0 +1,275 @@
1
+<template>
2
+  <view class="">
3
+    <view class="search-input-container" @click="toggle">
4
+      <view class="search-input placeholder" readonly>
5
+        {{ placeholder }}
6
+      </view>
7
+    </view>
8
+    <uni-popup ref="popup" type="bottom" background-color="#fff" borderRadius="8px" @change="change">
9
+      <view class="selected-area">
10
+        <view class="dialog-caption" hover-class="none" :hover-stop-propagation="false">
11
+          <view class="title-area">
12
+            <text class="dialog-title">{{ title }}</text>
13
+          </view>
14
+          <view class="dialog-close" @click="handleClose">
15
+            <view class="dialog-close-plus" data-id="close"></view>
16
+            <view class="dialog-close-plus dialog-close-rotate" data-id="close"></view>
17
+          </view>
18
+        </view>
19
+        <view class="inputView">
20
+          <SearchInput @search="search" :placeholder="placeholder"/>
21
+        </view>
22
+        <view class="content">
23
+          <scroll-view v-if="dataList && dataList.length" class="list" :scroll-y="true">
24
+            <view class="item" :class="{'is-disabled': !!item.disable}" v-for="(item, j) in dataList" :key="j"
25
+              @click="updateValue(item)">
26
+              <text class="item-text">{{item[map.text]}}</text>
27
+              <!-- 暂无多选需求 -->
28
+              <view class="check" v-if="false"></view>
29
+            </view>
30
+          </scroll-view>
31
+          <view v-else class="content-empty">
32
+            <u-loading-icon v-if="loading" text="加载中" textSize="18"></u-loading-icon>
33
+            <template v-else>
34
+              <view class="empty"></view>
35
+              <view class="empty-text">{{ placeholder }}</view>
36
+            </template>
37
+          </view>
38
+        </view>
39
+      </view>
40
+    </uni-popup>
41
+  </view>
42
+</template>
43
+
44
+<script>
45
+import SearchInput from './SelectData.vue'
46
+export default {
47
+  props: {
48
+    modelValue: {
49
+      type: String | Number | Array,
50
+      default: ''
51
+    },
52
+    load: {
53
+      type: Function,
54
+      default: () => Promise.resolve([])
55
+    },
56
+    map: {
57
+      type: Object,
58
+      default: ({ text: 'text', value: 'value' })
59
+    },
60
+    title: {
61
+      type: String,
62
+      default: '请选择'
63
+    },
64
+    placeholder: {
65
+      type: String,
66
+      default: '请输入'
67
+    },
68
+  },
69
+  components: { SearchInput },
70
+  data () {
71
+    return {
72
+      dataList: [],
73
+      loading: false,
74
+    }
75
+  },
76
+  methods: {
77
+    search (value) {
78
+      this.loading = true
79
+      this.load(value).then(res => {
80
+        this.dataList = res
81
+      }).finally(() => {
82
+        this.loading = false
83
+      })
84
+    },
85
+    toggle () {
86
+      this.$refs.popup.open()
87
+    },
88
+    handleClose () {
89
+      this.$refs.popup.close()
90
+    },
91
+    change (e) {
92
+      if (e.show) {
93
+        // this.search('')
94
+      } else {
95
+        this.loading = false
96
+        this.dataList = []
97
+      }
98
+    },
99
+    updateValue (item) {
100
+      this.$emit('update:modelValue', item[this.map.value])
101
+      this.$emit('change', item)
102
+      this.handleClose()
103
+    }
104
+  }
105
+}
106
+</script>
107
+
108
+<style lang="scss" scoped>
109
+.search-input-container {
110
+  position: relative;
111
+
112
+  .search-input {
113
+    width: 100%;
114
+    height: 35px;
115
+    border: 1px solid #ddd;
116
+    border-radius: 6px;
117
+    padding: 0 40px 0 12px;
118
+    font-size: 14px;
119
+    color: #333;
120
+    line-height: 35px;
121
+
122
+    &.placeholder {
123
+      font-size: 12px;
124
+      color: #808080;
125
+    }
126
+  }
127
+
128
+  .search-icon {
129
+    position: absolute;
130
+    right: 12px;
131
+    top: 50%;
132
+    transform: translateY(-50%);
133
+    font-size: 16px;
134
+  }
135
+}
136
+
137
+.selected-area {
138
+  height: 80vh;
139
+  overflow: hidden;
140
+  display: flex;
141
+  flex-direction: column;
142
+
143
+  .title-area {
144
+    /* #ifndef APP-NVUE */
145
+    display: flex;
146
+    /* #endif */
147
+    align-items: center;
148
+    /* #ifndef APP-NVUE */
149
+    margin: auto;
150
+    /* #endif */
151
+    padding: 0 10px;
152
+  }
153
+
154
+  .dialog-caption {
155
+    position: relative;
156
+    /* #ifndef APP-NVUE */
157
+    display: flex;
158
+    /* #endif */
159
+    flex-direction: row;
160
+    /* border-bottom: 1px solid #f0f0f0; */
161
+  }
162
+
163
+  .dialog-title {
164
+    /* font-weight: bold; */
165
+    line-height: 44px;
166
+  }
167
+
168
+  .dialog-close {
169
+    position: absolute;
170
+    top: 0;
171
+    right: 0;
172
+    bottom: 0;
173
+    /* #ifndef APP-NVUE */
174
+    display: flex;
175
+    /* #endif */
176
+    flex-direction: row;
177
+    align-items: center;
178
+    padding: 0 15px;
179
+  }
180
+
181
+  .dialog-close-plus {
182
+    width: 16px;
183
+    height: 2px;
184
+    background-color: #666;
185
+    border-radius: 2px;
186
+    transform: rotate(45deg);
187
+  }
188
+
189
+  .dialog-close-rotate {
190
+    position: absolute;
191
+    transform: rotate(-45deg);
192
+  }
193
+
194
+  .icon-clear {
195
+    display: flex;
196
+    align-items: center;
197
+  }
198
+  .inputView {
199
+    padding: 0 15px 10px;
200
+    border-bottom: 1px solid #f8f8f8;
201
+    margin: 2px;
202
+  }
203
+  .content {
204
+    overflow: hidden;
205
+    flex: 1;
206
+  }
207
+  .content-empty {
208
+    overflow: hidden;
209
+    margin-top: 10%;
210
+    height: 100%;
211
+    width: 100%;
212
+    display: flex;
213
+    align-items: center;
214
+    flex-direction: column;
215
+    row-gap: 20px;
216
+    color: #999;
217
+  }
218
+  .list {
219
+    width: 100%;
220
+    height: 100%;
221
+  }
222
+
223
+  .item {
224
+    padding: 12px 15px;
225
+    /* border-bottom: 1px solid #f0f0f0; */
226
+    /* #ifndef APP-NVUE */
227
+    display: flex;
228
+    /* #endif */
229
+    flex-direction: row;
230
+    justify-content: space-between;
231
+  }
232
+
233
+  .is-disabled {
234
+    opacity: .5;
235
+  }
236
+
237
+  .item-text {
238
+    /* flex: 1; */
239
+    color: #333333;
240
+  }
241
+
242
+  .item-text-overflow {
243
+    width: 280px;
244
+    /* fix nvue */
245
+    overflow: hidden;
246
+    /* #ifndef APP-NVUE */
247
+    width: 20em;
248
+    white-space: nowrap;
249
+    text-overflow: ellipsis;
250
+    -o-text-overflow: ellipsis;
251
+    /* #endif */
252
+  }
253
+
254
+	.check {
255
+		margin-right: 5px;
256
+		border: 2px solid #007aff;
257
+		border-left: 0;
258
+		border-top: 0;
259
+		height: 12px;
260
+		width: 6px;
261
+		transform-origin: center;
262
+		/* #ifndef APP-NVUE */
263
+		transition: all 0.3s;
264
+		/* #endif */
265
+		transform: rotate(45deg);
266
+	}
267
+  .empty {
268
+    margin: 0 auto;
269
+    width: 160px;
270
+    height: 155px;
271
+    background: url("../../../static/images/Empty.png") no-repeat;
272
+    background-size: cover;
273
+  }
274
+}
275
+</style>

+ 340 - 0
src/pages/attendance/components/SelectArea.vue

@@ -0,0 +1,340 @@
1
+<template>
2
+    <view class="selectAreaModal">
3
+        <view class="slot-content" @click.stop="openModal">
4
+            <slot></slot>
5
+        </view>
6
+        <u-popup :show="show" mode="center" :round="8">
7
+            <view class="modal-content">
8
+                <view class="title">
9
+                    <text>选择区域</text>
10
+                    <u-icon name="close" color="#666666" size="20" @click="close" />
11
+                </view>
12
+                <view class="terminal-list">
13
+
14
+                    <div class="tabs">
15
+                        <div v-for="(item, index) in terminals" :key="item.id"
16
+                            :class="{ active: currentTab === item.id }" class="tabs-item" @click="tabClick(item.id)">
17
+                            <u-checkbox-group @change="handleTerminalChange(item)">
18
+                                <u-checkbox v-model="item.checked" @change="handleTerminalCheck(item)"></u-checkbox>
19
+                            </u-checkbox-group>
20
+                            {{ item.label }}
21
+                        </div>
22
+                    </div>
23
+
24
+                    <view v-for="(terminal, index) in terminals" :key="terminal.id" class="area-list"
25
+                        v-show="currentTab === terminal.id">
26
+                        <u-checkbox-group>
27
+                            <view v-for="area in terminal.children" :key="area.id" class="area-item">
28
+                                <u-checkbox :label="area.label" :checked="area.checked"
29
+                                    @change="handleAreaCheck(area)"></u-checkbox>
30
+                            </view>
31
+                        </u-checkbox-group>
32
+                    </view>
33
+                </view>
34
+                <view class="footer-btn">
35
+                    <view class="custom-btn-normal" @click="submitSelectedAreas">确认选择</view>
36
+                </view>
37
+            </view>
38
+        </u-popup>
39
+    </view>
40
+</template>
41
+
42
+<script>
43
+import { formatTime } from '@/utils/formatUtils'
44
+import { dataConfigTree } from "@/api/attendance/attendance"
45
+
46
+export default {
47
+    components: {},
48
+    props: {
49
+        disabled: {
50
+            type: Boolean,
51
+            default: false
52
+        }
53
+    },
54
+    data() {
55
+        return {
56
+            show: false,
57
+            terminals: [],
58
+
59
+            currentTab: 0,
60
+            searchFocus: false,
61
+
62
+        }
63
+    },
64
+    computed: {
65
+
66
+    },
67
+    watch: {
68
+
69
+        show(newValue) {
70
+            if (newValue) {
71
+                const level = this.notkezhang ? 3 : 2
72
+
73
+                this.loadTerminals(level)
74
+            }
75
+        },
76
+        notkezhang: {
77
+            handler(newValue) {
78
+                const level = newValue ? 3 : 2
79
+
80
+                this.loadTerminals(level)
81
+            },
82
+            immediate: true
83
+        }
84
+    },
85
+    methods: {
86
+
87
+        handleTerminalChange(terminalObj) {
88
+            const curTerminal = this.terminals.find((item) => item.id == terminalObj.id)
89
+            for (let i = 0; i < curTerminal.children.length; i++) {
90
+                const child = curTerminal.children[i];
91
+                console.log(child, "child")
92
+                this.$set(child, 'checked', !child.checked)
93
+                this.$forceUpdate()
94
+            }
95
+        },
96
+        handleAreaCheck(area) {
97
+            area.checked = !area.checked
98
+        },
99
+        submitSelectedAreas() {
100
+            this.$emit('selected', this.terminals)
101
+            this.show = false
102
+        },
103
+        loadTerminals(level) {
104
+            dataConfigTree(level).then(res => {
105
+                console.log(res, "res")
106
+                this.terminals = res.data
107
+                this.currentTab = this.terminals[0].id
108
+            })
109
+
110
+        },
111
+        tabClick(id) {
112
+
113
+            this.currentTab = id;
114
+
115
+        },
116
+        handleTerminalCheck(terminal) {
117
+
118
+            this.$nextTick(() => {
119
+
120
+                const curItem = this.terminals.find((item) => item.id == terminal.id)
121
+                this.$set(curItem, 'checked', true)
122
+
123
+                for (let i = 0; i < curItem.children.length; i++) {
124
+                    this.$set(curItem.children[i], 'checked', !!curItem.children[i].checked)
125
+                }
126
+
127
+                this.$forceUpdate()
128
+            })
129
+
130
+
131
+        },
132
+        openModal() {
133
+            if (this.disabled) return
134
+            this.show = true
135
+        },
136
+        close() {
137
+            this.show = false
138
+        },
139
+    },
140
+}
141
+</script>
142
+
143
+<style lang="scss" scoped>
144
+.u-checkbox-group--row {
145
+    flex-direction: column !important;
146
+}
147
+
148
+.selectAreaModal {
149
+    .slot-content {}
150
+
151
+    .empty {
152
+        margin: 0 auto;
153
+        width: 160px;
154
+        height: 155px;
155
+        background: url("../../../static/images/Empty.png") no-repeat;
156
+        background-size: cover;
157
+    }
158
+
159
+    .modal-content {
160
+        width: 90vw;
161
+        max-height: 75vh;
162
+        padding: 10px 0;
163
+
164
+        .terminal-list {
165
+            padding: 0px 30rpx;
166
+
167
+            .tabs {
168
+                display: flex;
169
+                padding: 0rpx 0 0rpx;
170
+                margin-top: 32rpx;
171
+                font-size: 28rpx;
172
+                color: #666666;
173
+                line-height: 32rpx;
174
+                gap: 48rpx;
175
+
176
+                .tabs-item {
177
+                    display: flex;
178
+                    align-items: center;
179
+                    position: relative;
180
+
181
+                    &:after {
182
+                        content: "";
183
+                        position: absolute;
184
+                        bottom: -12rpx;
185
+                        left: 0;
186
+                        width: 50%;
187
+                        height: 6rpx;
188
+                        background: #fff;
189
+                        border-radius: 12rpx;
190
+                    }
191
+
192
+                    &.active {
193
+                        font-size: 36rpx;
194
+                        color: #2A70D1;
195
+                        line-height: 42rpx;
196
+
197
+                        &:after {
198
+                            background: #2A70D1;
199
+                        }
200
+                    }
201
+                }
202
+            }
203
+
204
+            .area-list {
205
+                padding: 15rpx 0;
206
+
207
+                .area-item {
208
+                    display: flex;
209
+                    height: 60rpx;
210
+                    line-height: 60rpx;
211
+                }
212
+            }
213
+        }
214
+
215
+        .title {
216
+            height: 24px;
217
+            font-weight: 400;
218
+            font-size: 18px;
219
+            color: #333333;
220
+            line-height: 21px;
221
+            display: flex;
222
+            justify-content: space-between;
223
+            align-items: center;
224
+            padding: 15px;
225
+            box-sizing: border-box;
226
+        }
227
+
228
+        .title-cell {
229
+            padding: 10px 15px;
230
+            box-sizing: border-box;
231
+
232
+            .title-text {
233
+                height: 16px;
234
+                font-weight: 400;
235
+                font-size: 14px;
236
+                color: #333333;
237
+                line-height: 16px;
238
+                text-align: left;
239
+                margin-bottom: 14px;
240
+                text-indent: 5px;
241
+            }
242
+        }
243
+
244
+        .personnel-list {
245
+            display: flex;
246
+            flex-wrap: wrap;
247
+            row-gap: 8px;
248
+            column-gap: 6px;
249
+
250
+            .personnel-item {
251
+                width: fit-content;
252
+                height: 34px;
253
+                background: #F0F0F0;
254
+                border-radius: 6px;
255
+                display: flex;
256
+                align-items: center;
257
+                column-gap: 6px;
258
+                padding: 0 5px;
259
+
260
+                .personnel-img {
261
+                    width: 24px;
262
+                    height: 24px;
263
+                    border-radius: 6px;
264
+                    overflow: hidden;
265
+                }
266
+
267
+                .personnel-name {
268
+                    width: fit-content;
269
+                    font-weight: 400;
270
+                    font-size: 13px;
271
+                    color: #3D3D3D;
272
+                    text-align: left;
273
+                    font-style: normal;
274
+                    text-transform: none;
275
+                }
276
+
277
+                .personnel-close {
278
+                    width: 16px;
279
+                }
280
+
281
+                .radio-button {
282
+                    width: 14px;
283
+                    height: 14px;
284
+                    border: 1px solid #2196F3;
285
+                    background: #fff;
286
+                    border-radius: 50%;
287
+                    position: relative;
288
+                    transition: border-color 0.2s ease;
289
+                }
290
+
291
+                /* 激活状态 - 蓝色边框 */
292
+                .radio-button.active {
293
+                    border-color: #496CF4;
294
+                }
295
+
296
+                /* 激活状态 - 中间的蓝点 */
297
+                .radio-button.active::after {
298
+                    content: '';
299
+                    position: absolute;
300
+                    top: 50%;
301
+                    left: 50%;
302
+                    transform: translate(-50%, -50%);
303
+                    width: 7px;
304
+                    height: 7px;
305
+                    background-color: #2196F3;
306
+                    border-radius: 50%;
307
+                }
308
+
309
+                /* 聚焦状态 */
310
+                .radio-button:focus {
311
+                    outline: none;
312
+                    box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.3);
313
+                }
314
+
315
+                /* 悬停效果 */
316
+                .radio-button:hover .radio-button:not(.active) {
317
+                    border-color: #999;
318
+                }
319
+
320
+                /* 禁用状态 */
321
+                .radio-button.disabled {
322
+                    opacity: 0.5;
323
+                    border-color: #999;
324
+                    background: #f0f0f0;
325
+                    cursor: not-allowed;
326
+                }
327
+            }
328
+        }
329
+
330
+        .footer-btn {
331
+            padding: 10px 15px;
332
+
333
+            .modal-slot-content {
334
+                white-space: pre;
335
+
336
+            }
337
+        }
338
+    }
339
+}
340
+</style>

+ 75 - 0
src/pages/attendance/components/SelectData.vue

@@ -0,0 +1,75 @@
1
+<template>
2
+  <view class="search-input-container">
3
+    <input
4
+      class="search-input"
5
+      v-model="searchKeyword"
6
+      :placeholder="placeholder"
7
+      @focus="inputFocus"
8
+      @blur="inputBlur"
9
+      @input="searchUsers"
10
+    />
11
+  </view>
12
+</template>
13
+
14
+<script>
15
+import { debounce } from '@/utils/handler'
16
+export default {
17
+  props: {
18
+    placeholder: {
19
+      type: String,
20
+      default: '请输入人员姓名'
21
+    },
22
+    
23
+  },
24
+  data () {
25
+    this.searchUsers = debounce(() => {
26
+      this.$emit('search', this.searchKeyword);
27
+    }, 200);
28
+    return {
29
+      searchKeyword: '',
30
+    }
31
+  },
32
+  
33
+  methods: {
34
+    inputFocus () {
35
+      this.$emit('searchViewShowEvent', true)
36
+    },
37
+    inputBlur () {
38
+      const searchViewShow = this.searchKeyword ? true : false
39
+      this.$emit('searchViewShowEvent', searchViewShow)
40
+    },
41
+    clearInput () {
42
+      this.searchKeyword = ''
43
+    }
44
+  },
45
+}
46
+</script>
47
+
48
+<style lang="scss" scoped>
49
+
50
+.search-input-container {
51
+  position: relative;
52
+
53
+  .search-input {
54
+    width: 100%;
55
+    height: 35px;
56
+    border: 1px solid #ddd;
57
+    border-radius: 6px;
58
+    padding: 0 40px 0 12px;
59
+    font-size: 14px;
60
+    color: #333;
61
+
62
+    &::placeholder {
63
+      color: #999;
64
+    }
65
+  }
66
+
67
+  .search-icon {
68
+    position: absolute;
69
+    right: 12px;
70
+    top: 50%;
71
+    transform: translateY(-50%);
72
+    font-size: 16px;
73
+  }
74
+}
75
+</style>

+ 59 - 0
src/pages/attendance/components/TimePicker.vue

@@ -0,0 +1,59 @@
1
+<template>
2
+  <view class="timePicker-container" @click="datetimePickerShow">
3
+    <input class="timePicker-input" type="text" v-model="viewValue" readonly />
4
+    <u-datetime-picker
5
+      :show="timePickerShow"
6
+      v-model="currentTime"
7
+      mode="time"
8
+      @confirm="datetimePickerConfirm"
9
+      @cancel="datetimePickerClose"
10
+      @close="datetimePickerClose"
11
+    />
12
+  </view>
13
+</template>
14
+
15
+<script>
16
+import { formatTime } from '@/utils/formatUtils'
17
+export default {
18
+  data () {
19
+    return {
20
+      timePickerShow: false,
21
+      currentTime: formatTime(new Date(), 'hh:mm'),
22
+      viewValue: formatTime(new Date(), 'hh:mm'),
23
+    }
24
+  },
25
+  methods: {
26
+    datetimePickerShow () {
27
+      this.timePickerShow = true
28
+    },
29
+    datetimePickerClose () {
30
+      this.timePickerShow = false
31
+    },
32
+    datetimePickerConfirm (result) {
33
+      const { value } = result
34
+
35
+      this.datetimePickerClose()
36
+    }
37
+  },
38
+}
39
+</script>
40
+
41
+<style lang="scss" scoped>
42
+.timePicker-container {
43
+  position: relative;
44
+
45
+  .timePicker-input {
46
+    width: 100%;
47
+    height: 35px;
48
+    border: 1px solid #ddd;
49
+    border-radius: 6px;
50
+    padding: 0 40px 0 12px;
51
+    font-size: 14px;
52
+    color: #333;
53
+
54
+    &::placeholder {
55
+      color: #999;
56
+    }
57
+  }
58
+}
59
+</style>

+ 55 - 0
src/pages/attendance/components/UserAvatar.vue

@@ -0,0 +1,55 @@
1
+<template>
2
+  <view class="user-avatar">
3
+    <image class="user-avatar-content" v-if="avatarLink && showImg" :src="avatarLink" @load="imageLoad" @error="imageLoadError" />
4
+    <view class="user-avatar-content" :style="{ background: getAvatarGradient(userName) }"  v-else>{{ getUserIndexChat() }}</view>
5
+  </view>
6
+</template>
7
+
8
+<script>
9
+import { getAvatarGradient } from '@/utils/handler'
10
+export default {
11
+  props: {
12
+    avatarLink: {
13
+      type: String,
14
+      default: ''
15
+    },
16
+    userName: {
17
+      type: String,
18
+      default: ''
19
+    }
20
+  },
21
+  data () {
22
+    return {
23
+      showImg: true
24
+    }
25
+  },
26
+  methods: {
27
+    getAvatarGradient,
28
+    getUserIndexChat () {
29
+      return this.userName ? this.userName.split('')[0] : '用'
30
+    },
31
+    imageLoad () {
32
+      this.showImg = true
33
+    },
34
+    imageLoadError () {
35
+      this.showImg = false
36
+    }
37
+  }
38
+}
39
+</script>
40
+
41
+<style lang="scss" scoped>
42
+.user-avatar {
43
+  width: 100%;
44
+  height: 100%;
45
+  &-content {
46
+    width: 100%;
47
+    height: 100%;
48
+    display: flex;
49
+    align-items: center;
50
+    justify-content: center;
51
+    color: #fff;
52
+    font-weight: 500;
53
+  }
54
+}
55
+</style>

+ 514 - 0
src/pages/attendance/components/WorkingGroup.vue

@@ -0,0 +1,514 @@
1
+<template>
2
+  <view class="workGroup-list">
3
+    <view class="working-group" v-for="({ data, info }, index) of historyList" :key="index">
4
+      <view>
5
+        <view class="title" v-if="!info.isCheckIn">{{ '开始上通道/负责区域' }}</view>
6
+        <view v-if="!info.isCheckIn" class="content">
7
+          <view class="empty" style="margin: 20px auto 0;"></view>
8
+          <view class="empty-text">当前未上通道/区域</view>
9
+        </view>
10
+        <view v-else class="content">
11
+          <view class="content-cell" v-if="notkezhang">
12
+            <text class="content-cell-label">通道</text>
13
+            <text class="content-cell-value">{{ info.channelName || info.channelCode || '--' }}</text>
14
+          </view>
15
+          <view class="content-cell" v-if="notkezhang">
16
+            <text class="content-cell-label">当班人员</text>
17
+            <text></text>
18
+          </view>
19
+          <view class="content-workings" v-if="notkezhang">
20
+            <view class="personnel-list" v-if="data.length">
21
+              <view class="personnel-item" v-for="item of data" :key="item.id">
22
+                <view class="personnel-img">
23
+                  <UserAvatar :userName="item.userName" :avatarLink="item.avatar" />
24
+                </view>
25
+                <view class="personnel-name">{{ item.userName }}</view>
26
+              </view>
27
+            </view>
28
+            <view class="empty" v-else></view>
29
+          </view>
30
+          <view class="content-cell" style="height: auto;display: flex;flex-direction: column;align-items: flex-start;"
31
+            v-if="!notkezhang">
32
+            <text class="content-cell-label">负责区域</text>
33
+            <!-- <text class="content-cell-value">{{ info.regionalName || info.regionalCode || '--' }}</text> -->
34
+            <view class="selected-areas"
35
+              v-if="getHandleAreaData(data) && getHandleAreaData(data).length && !notkezhang">
36
+              <view class="area-item" v-for="area in getHandleAreaData(data)" :key="area.code">
37
+                <view class="area-label">{{ area.label }}</view>
38
+                <text-tag :showClose="false" :tags="area.children" :labelColumn="'regionalName'" />
39
+              </view>
40
+            </view>
41
+          </view>
42
+          <view class="content-cell">
43
+            <text class="content-cell-label">上通道时间</text>
44
+            <text class="content-cell-value">{{ info.checkInTimeFormat }}</text>
45
+          </view>
46
+          <view class="content-cell" v-if="info.isCheckIn">
47
+            <text class="content-cell-label">下通道时间</text>
48
+            <text class="content-cell-value">{{ info.checkOutTime || '-:-' }}</text>
49
+          </view>
50
+        </view>
51
+      </view>
52
+      <!-- {{ selectedMember.length === 0 }}{{ !userInfo.roles.includes('kezhang') }}{{ !attendanceInfo.checkInTime }} -->
53
+      <!-- :disabled="!attendanceInfo.checkInTime || checkOutStatus" -->
54
+      <AddAttendancePersonnelModal v-if="index === 0" :disabled="checkOutStatus" :notkezhang="notkezhang"
55
+        :userInfo="userInfo" @updateRecord="invokerGetPostRecordList" :selectedMember="selectedMember" :attendanceInfo="attendanceInfo">
56
+        <!-- :class="{ disabled: !attendanceInfo.checkInTime }"   没有选人不可以上通道 -->
57
+        <view v-if="authority" class="custom-btn" @click="openModal"
58
+          :class="{ disabled: selectedMember.length === 0 && !userInfo.roles.includes('kezhang') || (!attendanceInfo.checkInTime || (attendanceInfo.checkInTime && attendanceInfo.checkOutTime)) }">
59
+          {{ checkOutStatus ? '下通道' : '上通道' }}
60
+        </view>
61
+      </AddAttendancePersonnelModal>
62
+    </view>
63
+    <u-popup :show="checkOutWork" mode="center" :round="8">
64
+      <view class="modal-content">
65
+        <view class="title">
66
+          <text>员工下通道</text>
67
+          <u-icon name="close" color="#666666" size="20" @click="close" />
68
+        </view>
69
+        <view class="title-cell">
70
+          <view class="title-text">{{ '选择下通道时间' }}</view>
71
+          <uni-datetime-picker type="datetime" v-model="currentTime" />
72
+        </view>
73
+        <view class="title-cell">
74
+          <view class="custom-btn-normal" @click="invokerUpdatePostRecord" :class="{ disabled: !currentTime }">确认下通道
75
+          </view>
76
+        </view>
77
+      </view>
78
+    </u-popup>
79
+  </view>
80
+</template>
81
+
82
+<script>
83
+import { listgroupbyTimeanduserid, updatePostRecord } from "@/api/attendance/attendance"
84
+import { formatTime, formatName } from '@/utils/formatUtils'
85
+import AddAttendancePersonnelModal from './AddAttendancePersonnelModal';
86
+import UserAvatar from './UserAvatar'
87
+export default {
88
+  components: { AddAttendancePersonnelModal, UserAvatar },
89
+  props: {
90
+    userInfo: {
91
+      type: Object,
92
+      default: () => ({})
93
+    },
94
+    attendanceInfo: { // 考勤信息
95
+      type: Object,
96
+      default: () => ({})
97
+    },
98
+    selectedMember: {
99
+      type: Array,
100
+      default: () => []
101
+    }
102
+  },
103
+  data() {
104
+    return {
105
+      curUserWorkingGroupData: {
106
+        passage: '',
107
+        checkInTime: '',
108
+        checkOutTime: '',
109
+        isCheckIn: false,
110
+      },
111
+      checkOutStatus: false,
112
+      checkOutWork: false,
113
+      currentTime: undefined,
114
+      workingGroupId: '',
115
+      historyList: [], //班组工作记录 todo 现在只能做科长
116
+
117
+    }
118
+  },
119
+  computed: {
120
+    notkezhang() {
121
+      return !this.userInfo.roles || !this.userInfo.roles.includes('kezhang')
122
+    },
123
+    authority() { // 上通道权限
124
+      return this.userInfo.roles && (
125
+        this.userInfo.roles.includes('kezhang') ||
126
+        this.userInfo.roles.includes('banzuzhang')
127
+      )
128
+    }
129
+  },
130
+  methods: {
131
+    openModal() {
132
+      if (!this.checkOutStatus) {
133
+        return;
134
+      }
135
+  
136
+      
137
+      this.currentTime = formatTime(new Date())
138
+      this.checkOutWork = true
139
+    },
140
+    close() {
141
+      this.currentTime = undefined
142
+      this.checkOutWork = false
143
+    },
144
+    isCheckOutJobs(curUserWorkingInfo = this.curUserWorkingGroupData) {
145
+
146
+      return Boolean(curUserWorkingInfo.checkInTime) && (!curUserWorkingInfo.checkOutTime || curUserWorkingInfo.checkOutTime === '2000-01-01 00:00:00')
147
+    },
148
+    getHandleAreaData(data) {
149
+      let louObj = {};
150
+      let areaArr = [];
151
+      data.forEach(element => {
152
+
153
+        let name = `${element.terminlName}-${element.terminlCode}`
154
+        if (!louObj[name]) {
155
+          louObj[name] = [];
156
+        }
157
+        louObj[name].push(element);
158
+      });
159
+
160
+      Object.keys(louObj).forEach(key => {
161
+        areaArr.push({
162
+          label: key.split('-')[0],
163
+          code: key.split('-')[1],
164
+          children: louObj[key]
165
+        });
166
+      });
167
+
168
+      return areaArr;
169
+    },
170
+    invokerGetPostRecordList(callback) {
171
+      return listgroupbyTimeanduserid().then(res => {
172
+        if (res.code === 200) {
173
+          const resData = (res.data || [])
174
+          this.historyList = resData.reduce((cur, item, index) => {
175
+            let info = {}
176
+            const workDataList = item.records.map((attr) => {
177
+              const result = {
178
+                ...attr,
179
+                isCheckIn: Boolean(attr.checkInTime), // 是否上过通道
180
+                checkInTimeFormat: formatTime(attr.checkInTime, 'hh:mm:ss'),
181
+                checkOutTime: attr.checkOutTime === '2000-01-01 00:00:00' ? '' : formatTime(attr.checkOutTime, 'hh:mm:ss'),
182
+                userName: formatName(attr.userName),
183
+              }
184
+              if (attr.userId === this.userInfo.userId) {
185
+                info = result
186
+              }
187
+
188
+              if (index === 0) { // 取最新纪录
189
+                this.curUserWorkingGroupData = info
190
+                this.workingGroupId = item.shiftCode
191
+              }
192
+              return result
193
+            })
194
+            cur.push({
195
+              info: info,
196
+              data: workDataList,
197
+              workingGroupId: item.shiftCode
198
+            })
199
+            return cur
200
+          }, [])
201
+
202
+
203
+
204
+
205
+
206
+          this.checkOutStatus = this.isCheckOutJobs(this.curUserWorkingGroupData)
207
+
208
+          if (!this.checkOutStatus) {
209
+            this.historyList = [{
210
+              info: {},
211
+              data: [],
212
+              workingGroupId: undefined
213
+            }, ...this.historyList]
214
+          }
215
+        }
216
+      }).catch(() => {
217
+        return Promise.resolve()
218
+      }).finally(() => {
219
+        callback && callback()
220
+      })
221
+    },
222
+
223
+    createRecordData(curUserRecordInfo) {
224
+      // 🔥 重要:计算工作时长
225
+      const checkTime = new Date(this.currentTime);
226
+      const workDurationMinutes = Math.floor((Date.now() - checkTime.getTime()) / 1000 / 60);
227
+      // 🔥 重要:构建更新数据
228
+      let res = [];
229
+      //如果是科长走多选逻辑,如果是班组长走单选逻辑
230
+      if (!this.notkezhang) {
231
+        console.log(this.historyList)
232
+
233
+        this.historyList.forEach(item => {
234
+          item.data.forEach(element => {
235
+            res.push(
236
+              {
237
+                // 基本字段
238
+                ...curUserRecordInfo,
239
+                isCheckIn: undefined,
240
+                revision: (curUserRecordInfo.revision || 1) + 1,
241
+                // 时间相关
242
+                checkInTime: typeof curUserRecordInfo.checkInTime === 'string' ? curUserRecordInfo.checkInTime : formatTime(new Date(curUserRecordInfo.checkInTime)),
243
+                attendanceDate: typeof curUserRecordInfo.attendanceDate === 'string' ? curUserRecordInfo.attendanceDate : formatTime(new Date(curUserRecordInfo.attendanceDate), 'YYYY-MM-DD'),
244
+                workDuration: workDurationMinutes, // 重新计算工作时长
245
+                overDuration: curUserRecordInfo.overDuration || 0,
246
+                // 组织架构信息
247
+                attendanceTeamId: curUserRecordInfo.attendanceTeamId || null,
248
+                attendanceTeamName: curUserRecordInfo.attendanceTeamName || '',
249
+                attendanceDepartmentId: curUserRecordInfo.attendanceDepartmentId || null,
250
+                attendanceDepartmentName: curUserRecordInfo.attendanceDepartmentName || '',
251
+                attendanceStationId: curUserRecordInfo.attendanceStationId || null,
252
+                attendanceStationName: curUserRecordInfo.attendanceStationName || '',
253
+                remark: curUserRecordInfo.remark || '手动添加上通道记录',
254
+                // 审计字段
255
+                createBy: curUserRecordInfo.createBy || (this.userInfo.userId + ''),
256
+                createTime: typeof curUserRecordInfo.createTime === 'string' ? curUserRecordInfo.createTime : formatTime(new Date(curUserRecordInfo.createTime)),
257
+                updateBy: (this.userInfo.userId + ''),
258
+                updateTime: formatTime(new Date()),
259
+                ...element,
260
+                checkOutTime: formatTime(checkTime), // 设置实际下岗时间
261
+              }
262
+            )
263
+          })
264
+        })
265
+      } else {
266
+        res = [{
267
+          // 基本字段
268
+          ...curUserRecordInfo,
269
+          isCheckIn: undefined,
270
+          revision: (curUserRecordInfo.revision || 1) + 1,
271
+          // 时间相关
272
+          checkInTime: typeof curUserRecordInfo.checkInTime === 'string' ? curUserRecordInfo.checkInTime : formatTime(new Date(curUserRecordInfo.checkInTime)),
273
+          checkOutTime: formatTime(checkTime), // 设置实际下岗时间
274
+          attendanceDate: typeof curUserRecordInfo.attendanceDate === 'string' ? curUserRecordInfo.attendanceDate : formatTime(new Date(curUserRecordInfo.attendanceDate), 'YYYY-MM-DD'),
275
+          workDuration: workDurationMinutes, // 重新计算工作时长
276
+          overDuration: curUserRecordInfo.overDuration || 0,
277
+          // 组织架构信息
278
+          attendanceTeamId: curUserRecordInfo.attendanceTeamId || null,
279
+          attendanceTeamName: curUserRecordInfo.attendanceTeamName || '',
280
+          attendanceDepartmentId: curUserRecordInfo.attendanceDepartmentId || null,
281
+          attendanceDepartmentName: curUserRecordInfo.attendanceDepartmentName || '',
282
+          attendanceStationId: curUserRecordInfo.attendanceStationId || null,
283
+          attendanceStationName: curUserRecordInfo.attendanceStationName || '',
284
+          remark: curUserRecordInfo.remark || '手动添加上通道记录',
285
+          // 审计字段
286
+          createBy: curUserRecordInfo.createBy || (this.userInfo.userId + ''),
287
+          createTime: typeof curUserRecordInfo.createTime === 'string' ? curUserRecordInfo.createTime : formatTime(new Date(curUserRecordInfo.createTime)),
288
+          updateBy: (this.userInfo.userId + ''),
289
+          updateTime: formatTime(new Date())
290
+        }]
291
+      }
292
+      console.log(res, "res")
293
+
294
+      return res;
295
+    },
296
+
297
+    invokerUpdatePostRecord() {
298
+      if (this.checkOutStatus) {
299
+        // 科长下区域
300
+        if (!this.notkezhang) {
301
+          uni.showLoading({ title: '正在下通道...' });
302
+          updatePostRecord(this.createRecordData(this.curUserWorkingGroupData)).then(res => {
303
+            if (res && res.code === 200) {
304
+              uni.showToast({
305
+                title: '下通道完成!',
306
+                icon: 'success',
307
+                duration: 2000
308
+              });
309
+            } else {
310
+              uni.showToast({
311
+                title: `下通道失败!${res.msg || '未知错误'}`,
312
+                icon: 'none',
313
+                duration: 2000
314
+              });
315
+            }
316
+            this.close()
317
+            return this.invokerGetPostRecordList()
318
+          }).catch(error => {
319
+            uni.showToast({
320
+              title: '下通道失败:' + (error.msg || '请重试'),
321
+              icon: 'error',
322
+              duration: 2000
323
+            });
324
+          }).finally(() => {
325
+            uni.hideLoading();
326
+          })
327
+        } else {// 当班人员下通道
328
+          uni.showLoading({ title: '正在批量下通道...' });
329
+          // 获取当前最新的工作组数据
330
+          const recordList = this.historyList.find(item => item.workingGroupId === this.workingGroupId) || { data: [] }
331
+          const promises = recordList.data.map(item => {
332
+            return updatePostRecord(this.createRecordData(item)).then(res => {
333
+              if (res && res.code === 200) {
334
+                return `${item.userName}: 下通道完成!`
335
+              } else {
336
+                return `${item.userName}下通道失败: ${res.msg || '未知错误'}`
337
+              }
338
+            }).catch((err) => {
339
+              return Promise.resolve(`${item.userName}下通道失败: 网络错误!`)
340
+            })
341
+          })
342
+          Promise.all(promises).then((res) => {
343
+            uni.showModal({
344
+              title: '批量下通道结果',
345
+              content: `${res.join('\n')}`,
346
+              showCancel: false,
347
+              confirmText: '确定'
348
+            });
349
+            this.close()
350
+            return this.invokerGetPostRecordList()
351
+          }).finally(() => {
352
+            uni.hideLoading();
353
+          })
354
+        }
355
+      }
356
+    }
357
+  },
358
+}
359
+</script>
360
+
361
+<style lang="scss" scoped>
362
+.workGroup-list {
363
+  display: flex;
364
+  flex-direction: column;
365
+  row-gap: 15px;
366
+}
367
+
368
+.working-group {
369
+  width: 100%;
370
+  background: #FFFFFF;
371
+  box-shadow: 0px 4px 10px 0px rgba(0, 0, 0, 0.08);
372
+  border-radius: 16px 16px 16px 16px;
373
+  border: 1px solid #F0F8FF;
374
+  padding: 15px;
375
+  box-sizing: border-box;
376
+
377
+  .empty {
378
+    margin: 0 auto;
379
+    width: 160px;
380
+    height: 155px;
381
+    background: url("../../../static/images/Empty.png") no-repeat;
382
+    background-size: cover;
383
+  }
384
+
385
+  .empty-text {
386
+    font-weight: 400;
387
+    font-size: 14px;
388
+    color: #3D3D3D;
389
+    line-height: 16px;
390
+    text-align: center;
391
+    font-style: normal;
392
+    text-transform: none;
393
+    margin-bottom: 20px;
394
+  }
395
+
396
+  .title {
397
+    font-weight: 400;
398
+    font-size: 18px;
399
+    color: #333333;
400
+    line-height: 24px;
401
+    text-align: left;
402
+    font-style: normal;
403
+    text-transform: none;
404
+    padding: 5px 0;
405
+  }
406
+
407
+  .content-cell {
408
+    display: flex;
409
+    align-items: center;
410
+    justify-content: space-between;
411
+    font-weight: 400;
412
+    font-size: 14px;
413
+    font-style: normal;
414
+    text-transform: none;
415
+    width: 100%;
416
+    height: 36px;
417
+
418
+    .content-cell-label {
419
+      color: #222222;
420
+    }
421
+
422
+    .content-cell-value {
423
+      color: #999999;
424
+      padding-right: 5px;
425
+    }
426
+
427
+    .selected-areas {
428
+      .area-item {
429
+        .area-label {
430
+          height: 72rpx;
431
+          line-height: 72rpx;
432
+        }
433
+      }
434
+    }
435
+
436
+
437
+  }
438
+
439
+  .content-workings {
440
+    display: flex;
441
+    padding: 10px 0;
442
+
443
+    .personnel-list {
444
+      display: flex;
445
+      flex-wrap: wrap;
446
+      row-gap: 10px;
447
+      column-gap: 10px;
448
+
449
+      .personnel-item {
450
+        width: fit-content;
451
+        height: 34px;
452
+        background: #F0F0F0;
453
+        border-radius: 6px;
454
+        display: flex;
455
+        align-items: center;
456
+        column-gap: 8px;
457
+        padding: 0 10px;
458
+
459
+        .personnel-img {
460
+          width: 24px;
461
+          height: 24px;
462
+          border-radius: 6px;
463
+          overflow: hidden;
464
+        }
465
+
466
+        .personnel-name {
467
+          width: fit-content;
468
+          font-weight: 400;
469
+          font-size: 13px;
470
+          color: #3D3D3D;
471
+          text-align: left;
472
+          font-style: normal;
473
+          text-transform: none;
474
+        }
475
+      }
476
+    }
477
+  }
478
+}
479
+
480
+.modal-content {
481
+  width: 90vw;
482
+  max-height: 75vh;
483
+  padding: 10px 0;
484
+
485
+  .title {
486
+    height: 24px;
487
+    font-weight: 400;
488
+    font-size: 18px;
489
+    color: #333333;
490
+    line-height: 21px;
491
+    display: flex;
492
+    justify-content: space-between;
493
+    align-items: center;
494
+    padding: 15px;
495
+    box-sizing: border-box;
496
+  }
497
+
498
+  .title-cell {
499
+    padding: 10px 15px;
500
+    box-sizing: border-box;
501
+
502
+    .title-text {
503
+      height: 16px;
504
+      font-weight: 400;
505
+      font-size: 14px;
506
+      color: #333333;
507
+      line-height: 16px;
508
+      text-align: left;
509
+      margin-bottom: 14px;
510
+      text-indent: 5px;
511
+    }
512
+  }
513
+}
514
+</style>

+ 376 - 0
src/pages/attendance/index.vue

@@ -0,0 +1,376 @@
1
+<template>
2
+  <home-container :customStyle="{ backgroundPositionY: '-50px' }">
3
+    <view class="attendance-container">
4
+      <!-- 顶部个人信息栏 -->
5
+      <view class="user-header" v-if="currentUser">
6
+        <view class="user-avatar">
7
+          <image v-if="currentUser.avatar" :src="currentUser.avatar" class="cu-avatar xl round"></image>
8
+        </view>
9
+        <view class="user-info">
10
+          <text class="user-name">{{ `${userInfoAndRoles.nickName} ${userInfoAndRoles.userName}` }}</text>
11
+          <text v-if="userInfo.userInfo.stationName" class="position-item">{{ userInfo.userInfo.stationName }}</text>
12
+          <text v-if="userInfo.userInfo.stationName && userInfo.userInfo.departmentName" class="separator">/</text>
13
+          <text v-if="userInfo.userInfo.departmentName" class="position-item">{{
14
+            userInfo.userInfo.departmentName }}</text>
15
+          <text v-if="userInfo.userInfo.departmentName && userInfo.userInfo.teamsName" class="separator">/</text>
16
+          <text v-if="userInfo.userInfo.teamsName" class="position-item">{{ userInfo.userInfo.teamsName }}</text>
17
+        </view>
18
+        <view class="stats-btn" @click="showStats">
19
+          <uni-icons type="calendar" size="24" color="#666"></uni-icons>
20
+          <text class="btn-text">打卡统计</text>
21
+        </view>
22
+      </view>
23
+
24
+      <!-- 新界面打卡 -->
25
+      <AttendanceControl :attendanceInfo="attendanceInfo" :checkInPosiInfo="checkInPosiInfo"
26
+        @attendanceHandler="handleCheckIn" />
27
+      <!-- 工作区域/班组成员 -->
28
+      <MaintainAreaOrMemberModal ref="maintainAreaOrMemberModal" v-if="showMaintain" :userInfo="userInfoAndRoles"
29
+        @update-member="updateMember" :attendanceInfo="attendanceInfo" />
30
+      <!-- 新界面工作组状态 -->
31
+      <WorkingGroup ref="workingGroup" style="margin: 16px 0 24px 0" :attendanceInfo="attendanceInfo"
32
+        :loadUserInfoOver="loadUserInfoOver" :userInfo="userInfoAndRoles" :selectedMember="selectedMember" />
33
+    </view>
34
+  </home-container>
35
+</template>
36
+
37
+<script>
38
+import HomeContainer from "@/components/HomeContainer.vue";
39
+import AttendanceControl from './components/AttendanceControl'
40
+import MaintainAreaOrMemberModal from './components/MaintainAreaOrMemberModal'
41
+import WorkingGroup from './components/WorkingGroup'
42
+import { addAttendance, areaList } from "@/api/attendance/attendance"
43
+import { getInfo } from "@/api/login"
44
+import { formatTime as formatTimehandler } from '@/utils/formatUtils'
45
+import { getAttendanceList, queryClickAble } from "@/api/attendance/attendance"
46
+import { isInRangeOptimized, wgs84ToGcj02 } from '@/utils/handler'
47
+
48
+export default {
49
+  components: { HomeContainer, AttendanceControl, WorkingGroup, MaintainAreaOrMemberModal },
50
+
51
+  computed: {
52
+    currentUser() {
53
+      return this.$store.state.user;
54
+    },
55
+    userInfoAndRoles() {
56
+      return {
57
+        ...this.userInfo.userInfo,
58
+        roles: this.userInfo.roles
59
+      }
60
+    },
61
+    showMaintain() {
62
+      return this.userInfo.roles.includes('banzuzhang')
63
+    }
64
+  },
65
+  data() {
66
+    return {
67
+      yesterday: false,
68
+      checkInType: 'morning', // morning/afternoon
69
+      isCheckedIn: false,
70
+      checkInBtnText: '打卡',
71
+      currentTime: this.formatTime(new Date()),
72
+      locationIndex: 0,
73
+
74
+      locations: [
75
+        '民航机场',
76
+        '东方雨虹新材料装备研发总部基地',
77
+        '北京阳光汇点数码总部',
78
+        '海淀区研发中心'
79
+      ],
80
+      groupIndex: 0,
81
+      groups: [
82
+        '技术部',
83
+        '产品部',
84
+        '运营部',
85
+        '市场部'
86
+      ],
87
+      records: [],          // 今日真实打卡列表
88
+      yesterdayRecords: [], // 昨天真实打卡列表
89
+      userInfo: {
90
+        userInfo: {},
91
+        roles: []
92
+      },
93
+      checkInPosiInfo: [],
94
+      attendanceInfo: {
95
+        attendanceDate: '', // 当前日期
96
+        checkInTime: '', //上班打卡时间
97
+        checkOutTime: '', //下班打卡时间
98
+        items: [], // 打开记录 第一条为最早上班打卡时间 最后一条为最晚下班打卡时间
99
+      },
100
+      loadUserInfoOver: false,
101
+      selectedMember: [],
102
+      result: null
103
+    }
104
+  },
105
+  mounted() {
106
+    this.updateCurrentTime();
107
+    this.checkCheckInStatus();
108
+
109
+    // app 在前台时获取位置信息
110
+    uni.onLocationChange((res) => {
111
+      this.result = { latitude: res.latitude, longitude: res.longitude }
112
+    });
113
+  },
114
+  async onLoad() {
115
+    this.loadUserInfoOver = false
116
+    areaList().then(res => {
117
+      this.checkInPosiInfo = res.data.map(item => {
118
+        return {
119
+          longitude: item.longitude,
120
+          latitude: item.latitude,
121
+          radius: item.radius,
122
+          address: item.address
123
+        }
124
+      })
125
+    })
126
+    const userInfo = await getInfo()
127
+    this.userInfo = userInfo || {}
128
+
129
+    this.loadUserInfoOver = true
130
+    this.$nextTick(() => {
131
+      this.$refs.workingGroup.invokerGetPostRecordList()
132
+    })
133
+    // await this.loadYesterdayRecord();  // 1. 先查昨天
134
+    // this.updateCurrentTime();          // 2. 再开始计时
135
+    await this.loadTodayRecord();      // 3. 查今天
136
+
137
+  },
138
+  methods: {
139
+
140
+    updateMember(member) {
141
+      this.selectedMember = member
142
+    },
143
+    async loadYesterdayRecord() {
144
+      const yesterday = new Date();
145
+      yesterday.setDate(yesterday.getDate() - 1);
146
+      const start = this.formatDate(yesterday);
147
+
148
+      const { data } = await getAttendanceList({ type: 1, checkInDate: start, userId: this.$store.state.user.id });
149
+      this.yesterdayRecords = data?.[0]?.items || [];
150
+      // this.yesterdayRecords = data[0].items || [];
151
+
152
+      // 逻辑:昨天缺下班 && 距上班 ≤ 20 小时
153
+      const lastMorning = this.yesterdayRecords.find(r => r.checkInType == 'CLOCK_IN'); // 1-上班
154
+      const lastEvening = this.yesterdayRecords.find(r => r.checkInType == 'CLOCK_OUT'); // 2-下班
155
+
156
+      if (lastMorning && !lastEvening) {
157
+        // 确保时间字符串格式正确
158
+        const checkInTime = new Date(lastMorning.checkInTime);
159
+        if (isNaN(checkInTime.getTime())) {
160
+          console.error('无效的时间格式');
161
+          return;
162
+        }
163
+
164
+        // 计算时间差(毫秒)
165
+        // const gap = Date.now() - checkInTime.getTime();
166
+        const gap = Date.now() - new Date(lastMorning.checkInTime).getTime();
167
+        if (gap <= 12 * 60 * 60 * 1000) {
168
+          this.yesterday = true;
169
+          // 12 小时内
170
+          this.checkInType = 'afternoon';
171
+          this.checkInBtnText = '下班打卡';
172
+
173
+          this.records = this.yesterdayRecords.map(it => ({
174
+            type: it.checkInType === 'CLOCK_IN' ? '上班打卡' : '下班打卡',
175
+            time: it.checkInTime,   // 取 HH:mm:ss
176
+            location: it.checkInPosition || '--',
177
+            status: '正常',                  // 如需迟到逻辑,可在此判断
178
+            checkInDate: it.checkInDate
179
+          }));
180
+          if (this.yesterdayRecords.length > 0) {
181
+            this.checkInType = 'afternoon'
182
+          }
183
+
184
+          // 刷新按钮状态
185
+          this.checkCheckInStatus();
186
+        }
187
+      }
188
+    },
189
+    async loadTodayRecord() {
190
+      if (this.yesterday) {
191
+        return;
192
+      }
193
+      const today = this.formatDate(new Date());
194
+      const { data } = await getAttendanceList({
195
+        type: 1,
196
+        checkInDate: today,
197
+        userId: this.$store.state.user.id
198
+      });
199
+      let res = await queryClickAble()
200
+      let checkAbleType = res.data;
201
+   
202
+      // new
203
+      if (Array.isArray(data) && data.length) {
204
+        const items = data[0]
205
+        const firstCheckOnTime = items.items.filter(item => item.checkInType === 'CLOCK_IN' && item.checkInTime).sort((a, b) => {
206
+          return new Date(a.checkInTime).getTime() - new Date(b.checkInTime)
207
+        })[0] || {}
208
+        const latestCheckOutTime = items.items.filter(item => item.checkInType === 'CLOCK_OUT').sort((a, b) => {
209
+          return new Date(b.checkInTime) - new Date(a.checkInTime).getTime()
210
+        })[0] || {}
211
+        console.log(firstCheckOnTime,"firstCheckOnTime")
212
+
213
+        this.attendanceInfo = {
214
+          ...items,
215
+          ...(checkAbleType == '1'? {checkInTime: '', checkOutTime: ''}: {}),
216
+          ...(checkAbleType == '2'? {checkInTime: formatTimehandler(firstCheckOnTime.checkInTime, 'hh:mm:ss', { defaultResult: '' }), checkOutTime: ''}:{}),
217
+          // checkInTime: checkAbleType == 1 ? '' : formatTimehandler(firstCheckOnTime.checkInTime, 'hh:mm:ss', { defaultResult: '' }),
218
+          // checkOutTime: checkAbleType == 1 ? '' : formatTimehandler(latestCheckOutTime.checkInTime, 'hh:mm:ss', { defaultResult: '' })
219
+        }
220
+        console.log(this.attendanceInfo,"this.attendanceInfo")
221
+        
222
+      }
223
+
224
+      // 接口 data 可能是多条(按天),我们取第一条里的 items
225
+      const todayData = data && data.length ? data[0] : null;
226
+      const items = todayData ? todayData.items : [];
227
+
228
+      // 映射成页面需要的格式
229
+      this.records = items.map(it => ({
230
+        type: it.checkInType === 'CLOCK_IN' ? '上班打卡' : '下班打卡',
231
+        time: it.checkInTime,   // 取 HH:mm:ss
232
+        location: it.checkInPosition || '--',
233
+        status: '正常',
234
+        checkInDate: it.checkInDate
235
+
236
+      }));
237
+      if (items.length > 0) {
238
+        this.checkInType = 'afternoon'
239
+      }
240
+
241
+      // 刷新按钮状态
242
+      this.checkCheckInStatus();
243
+    },
244
+
245
+    // 通用时间格式化
246
+    formatTime(date) {
247
+      const h = String(date.getHours()).padStart(2, '0');
248
+      const m = String(date.getMinutes()).padStart(2, '0');
249
+      const s = String(date.getSeconds()).padStart(2, '0');
250
+      return `${h}:${m}:${s}`;
251
+    },
252
+
253
+    // 日期格式化(昨天、今天查询用)
254
+    formatDate(date) {
255
+      const y = date.getFullYear();
256
+      const m = String(date.getMonth() + 1).padStart(2, '0');
257
+      const d = String(date.getDate()).padStart(2, '0');
258
+      return `${y}-${m}-${d}`;
259
+    },
260
+    updateCurrentTime() {
261
+      setInterval(() => {
262
+        this.currentTime = this.formatTime(new Date());
263
+
264
+        // 自动切换上下班打卡类型
265
+        // const hours = new Date().getHours();
266
+        // this.checkInType = hours < 12 ? 'morning' : 'afternoon';
267
+        this.checkInBtnText = this.isCheckedIn ? '已打卡' : '打卡';
268
+      }, 1000);
269
+    },
270
+    checkCheckInStatus() {
271
+      const now = new Date();
272
+      const type = now.getHours() < 12 ? 1 : 2;
273
+      this.isCheckedIn = this.records.some(r => r.type === type);
274
+      this.checkInBtnText = this.isCheckedIn ? '已打卡' : '打卡';
275
+    },
276
+    changeGroup(e) {
277
+      this.groupIndex = e.detail.value;
278
+    },
279
+    async handleCheckIn(type, checkInPosition) {
280
+      if (this.isCheckedIn) return;
281
+      // if (this.attendanceInfo.checkInTime) { // 上班有卡 没下通道禁止打卡
282
+      //   if (this.$refs.workingGroup.isCheckOutJobs()) {
283
+      //     return uni.showToast({ title: '请先下岗位再下班打卡!', icon: 'none' });
284
+      //   }
285
+      // } 这个限制 业务不要
286
+      uni.showLoading({ title: '打卡中...' });
287
+      try {
288
+        await addAttendance({
289
+          userId: this.$store.state.user.id,
290
+          checkInDate: this.formatDate(new Date()),
291
+          checkInType: type, // 1 上班 2 下班
292
+          remark: '',
293
+          checkInPosition: checkInPosition.address,
294
+          longitude: checkInPosition.longitude,
295
+          latitude: checkInPosition.latitude,
296
+        });
297
+
298
+        await this.loadTodayRecord(); // 重新拉取今日
299
+        uni.showToast({ title: '打卡成功', icon: 'success' });
300
+      } finally {
301
+        uni.hideLoading();
302
+      }
303
+    },
304
+    showStats() {
305
+      uni.navigateTo({ url: '/pages/attendance/stats' });
306
+    }
307
+  }
308
+
309
+}
310
+</script>
311
+
312
+<style lang="scss" scoped>
313
+.attendance-container {
314
+  padding: 5px;
315
+  padding-top: 60px;
316
+  min-height: 100vh;
317
+}
318
+
319
+/* 顶部个人信息 */
320
+.user-header {
321
+  display: flex;
322
+  align-items: center;
323
+  background-color: #fff;
324
+  padding: 15px;
325
+  margin-bottom: 20px;
326
+  height: 96px;
327
+  background: #FFFFFF;
328
+  box-shadow: 0px 4px 16px 0px rgba(0, 0, 0, 0.06);
329
+  border-radius: 16px 16px 16px 16px;
330
+
331
+  .user-avatar {
332
+    width: 50px;
333
+    height: 50px;
334
+    border-radius: 25px;
335
+    overflow: hidden;
336
+    margin-right: 12px;
337
+
338
+    image {
339
+      width: 100%;
340
+      height: 100%;
341
+    }
342
+  }
343
+
344
+  .user-info {
345
+    flex: 1;
346
+
347
+    .user-name {
348
+      font-size: 18px;
349
+      font-weight: bold;
350
+      color: #333;
351
+      display: block;
352
+      margin-bottom: 8px;
353
+    }
354
+
355
+    .user-position {
356
+      font-size: 14px;
357
+      color: #666;
358
+    }
359
+  }
360
+
361
+  .stats-btn {
362
+    display: flex;
363
+    flex-direction: column;
364
+    align-items: center;
365
+    justify-content: center;
366
+    font-size: 14px;
367
+    color: #666;
368
+
369
+    .btn-text {
370
+      margin-top: 4px;
371
+      font-size: 12px;
372
+      color: #666;
373
+    }
374
+  }
375
+}
376
+</style>

+ 214 - 0
src/pages/attendance/stats.vue

@@ -0,0 +1,214 @@
1
+<template>
2
+  <view class="stats-container">
3
+    <!-- 用户信息 -->
4
+    <view class="user-header">
5
+      <view class="user-avatar">
6
+        <image v-if="currentUser.avatar" :src="currentUser.avatar" class="cu-avatar xl round"></image>
7
+      </view>
8
+      <view class="user-info">
9
+        <text class="user-name">{{ currentUser.name }}</text>
10
+      </view>
11
+    </view>
12
+
13
+    <!-- 日历选择器 -->
14
+    <uni-calendar class="uni-calendar--hook card-radius" :selected="markedDates" :showMonth="true"
15
+      @change="handleDateChange" @monthSwitch="handleMonthSwitch" />
16
+
17
+    <!-- 打卡记录展示 -->
18
+    <TimelineRecord class="card-radius" :records="filteredRecords" v-if="filteredRecords.length > 0" />
19
+    <view v-else class="no-data">
20
+      <text>暂无打卡记录</text>
21
+    </view>
22
+  </view>
23
+</template>
24
+
25
+<script>
26
+import TimelineRecord from '@/components/timeline-record/TimelineRecord.vue';
27
+import { getAttendanceList } from "@/api/attendance/attendance"
28
+
29
+export default {
30
+  components: {
31
+    TimelineRecord
32
+  },
33
+  computed: {
34
+    currentUser() {
35
+      return this.$store.state.user || {
36
+        name: '未知用户',
37
+        department: '未知部门',
38
+        position: '未知职位'
39
+      };
40
+    }
41
+  },
42
+  data() {
43
+    return {
44
+      currentDate: new Date(),
45
+      markedDates: [],
46
+      allRecords: [],
47
+      filteredRecords: []
48
+    };
49
+  },
50
+  mounted() {
51
+    this.loadAttendanceData();
52
+  },
53
+  methods: {
54
+    async loadAttendanceData() {
55
+      try {
56
+        const { startDate, endDate } = this.getMonthRange();
57
+        const res = await getAttendanceList({
58
+          userId: this.$store.state.user.id,
59
+          type: 2,
60
+          startDate,
61
+          endDate
62
+        });
63
+
64
+        if (res.code === 200) {
65
+          this.allRecords = res.data;
66
+          this.updateMarkedDates();
67
+          this.filterRecordsByDate(this.currentDate);
68
+        }
69
+      } catch (error) {
70
+        console.error('加载考勤数据失败:', error);
71
+        uni.showToast({
72
+          title: '数据加载失败',
73
+          icon: 'none'
74
+        });
75
+      }
76
+    },
77
+
78
+    
79
+updateMarkedDates() {
80
+  const today = new Date().toISOString().split('T')[0];
81
+  this.markedDates = this.allRecords
82
+    .filter(record => record.attendanceDate)
83
+    .map(record => {
84
+		console.log(record,'12312313213')
85
+      const isToday = record.attendanceDate.split('T')[0] === today;
86
+      const hasCheckIn = !!record.checkInTime;
87
+      const hasCheckOut = !!record.checkOutTime;
88
+      
89
+      let status;
90
+      let custom = '';
91
+      if (isToday) {
92
+        status = ''; // 当天记录始终显示正常
93
+      } else if (hasCheckIn && hasCheckOut) {
94
+        status = '正常'; // 完整打卡记录
95
+		custom = 'blue'
96
+      } else if (hasCheckIn) {
97
+        status = '异常'; // 只有上班打卡
98
+      } else {
99
+        return null; // 无效记录过滤
100
+      }
101
+
102
+      return {
103
+        date: record.attendanceDate.split('T')[0],
104
+        info: status,
105
+		color:custom
106
+      };
107
+    })
108
+    .filter(Boolean); // 移除null值
109
+},
110
+
111
+
112
+    filterRecordsByDate(date) {
113
+      const dateStr = this.formatDate(date);
114
+      
115
+      this.filteredRecords = this.allRecords.filter(
116
+        record => record.attendanceDate.includes(dateStr)
117
+      );
118
+    },
119
+
120
+    getMonthRange() {
121
+      const year = this.currentDate.getFullYear();
122
+      const month = this.currentDate.getMonth();
123
+      return {
124
+        startDate: this.formatDate(new Date(year, month, 1)),
125
+        endDate: this.formatDate(new Date(year, month + 1, 0))
126
+      };
127
+    },
128
+
129
+    formatDate(date) {
130
+      const y = date.getFullYear();
131
+      const m = String(date.getMonth() + 1).padStart(2, '0');
132
+      const d = String(date.getDate()).padStart(2, '0');
133
+      return `${y}-${m}-${d}`;
134
+    },
135
+
136
+    handleDateChange(detail) {
137
+      if (!detail || !detail.fulldate) {
138
+        console.error('日期参数异常:', detail);
139
+        return;
140
+      }
141
+      this.currentDate = new Date(detail.fulldate);
142
+      this.filterRecordsByDate(this.currentDate);
143
+    },
144
+
145
+    handleMonthSwitch({ month }) {
146
+      this.currentDate = new Date(
147
+        this.currentDate.getFullYear(),
148
+        month - 1,
149
+        this.currentDate.getDate()
150
+      );
151
+      this.loadAttendanceData();
152
+    }
153
+  }
154
+};
155
+</script>
156
+
157
+<style lang="scss" scoped>
158
+.stats-container {
159
+  padding: 20rpx;
160
+  background-color: #f7f7f7;
161
+  min-height: 100vh;
162
+}
163
+
164
+.user-header {
165
+  display: flex;
166
+  align-items: center;
167
+  padding: 20rpx;
168
+  margin-bottom: 20rpx;
169
+  background-color: #fff;
170
+  border-radius: 16rpx;
171
+
172
+  .user-avatar {
173
+    width: 100rpx;
174
+    height: 100rpx;
175
+    border-radius: 50%;
176
+    overflow: hidden;
177
+    margin-right: 20rpx;
178
+
179
+    image {
180
+      width: 100%;
181
+      height: 100%;
182
+    }
183
+  }
184
+
185
+  .user-info {
186
+    flex: 1;
187
+
188
+    .user-name {
189
+      font-size: 36rpx;
190
+      font-weight: bold;
191
+      color: #333;
192
+    }
193
+
194
+    .user-position {
195
+      font-size: 28rpx;
196
+      color: #999;
197
+    }
198
+  }
199
+}
200
+
201
+.card-radius {
202
+  border-radius: 16rpx;
203
+  overflow: hidden;
204
+  margin-bottom: 20rpx;
205
+}
206
+
207
+.no-data {
208
+  padding: 40rpx;
209
+  text-align: center;
210
+  color: #999;
211
+  background-color: #fff;
212
+  border-radius: 16rpx;
213
+}
214
+</style>

+ 78 - 0
src/pages/attendanceStatistics/components/TableList.vue

@@ -0,0 +1,78 @@
1
+<template>
2
+  <div class="table-container">
3
+    <table class="data-table">
4
+      <thead>
5
+        <tr>
6
+          <th v-for="column in columns" :key="column.props">{{ column.title }}</th>
7
+        </tr>
8
+      </thead>
9
+      <tbody>
10
+        <tr v-for="(row, index) in data" :key="index"
11
+          :class="{ 'odd-row': (index + 1) % 2 !== 0, 'even-row': (index + 1) % 2 === 0 }">
12
+          
13
+          <td v-for="column in columns" :key="`${column.props}-${index}`">{{ row[column.props] }}</td>
14
+        </tr>
15
+      </tbody>
16
+    </table>
17
+  </div>
18
+</template>
19
+
20
+<script>
21
+export default {
22
+  name: 'TableList',
23
+  props: {
24
+    columns: {
25
+      type: Array,
26
+      default: () => []
27
+
28
+    },
29
+    data: {
30
+      type: Array,
31
+      default: () => []
32
+    }
33
+  },
34
+  created() {
35
+    console.log(this.columns,this.data);
36
+  }
37
+};
38
+</script>
39
+
40
+<style lang="scss" scoped>
41
+.table-container {
42
+  width: 100%;
43
+  overflow-x: auto;
44
+}
45
+
46
+.data-table {
47
+  width: 100%;
48
+  border-collapse: collapse;
49
+  border: 4rpx dashed #ccc;
50
+  border-radius: 16rpx;
51
+
52
+  th,
53
+  td {
54
+    padding: 12rpx;
55
+    height: 48rpx;
56
+    text-align: left;
57
+    border-bottom: 1rpx dashed #ccc;
58
+  }
59
+
60
+  th {
61
+    background-color: #f5f5f5;
62
+    font-weight: bold;
63
+    color: #666;
64
+  }
65
+
66
+  .odd-row {
67
+    background-color: #fff !important;
68
+  }
69
+
70
+  .even-row {
71
+    background-color: #f5f5f5 !important;
72
+  }
73
+
74
+  tr:hover {
75
+    background-color: #e0e0e0;
76
+  }
77
+}
78
+</style>

+ 549 - 0
src/pages/attendanceStatistics/index.vue

@@ -0,0 +1,549 @@
1
+<template>
2
+    <home-container :customStyle="{ background: 'none' }">
3
+        <view class="container">
4
+            <!-- 顶部tabs -->
5
+            <h-tabs v-if="roles.includes('kezhang')" v-model="currentTab" :tabs="tabList" value-key="type"
6
+                label-key="title" @change="handleTabChange" />
7
+
8
+            <!-- 通道开放情况 -->
9
+            <view class="card" :style="{ paddingBottom: currentTab !== 'all' ? '100rpx' : '24rpx' }">
10
+                <view class="card-header">通道开放情况</view>
11
+                <view class="chart-container">
12
+                    <view class="pie-chart" id="pieChart"></view>
13
+                </view>
14
+
15
+                <view v-if="currentTab === 'all'" style="margin-top: 120rpx;" class="bar-chart" id="barChart1"></view>
16
+            </view>
17
+
18
+            <!-- 人员情况 -->
19
+            <view v-if="currentTab === 'all'" class="card">
20
+                <view class="card-header">人员情况</view>
21
+                <view class="bar-chart" id="barChart2"></view>
22
+            </view>
23
+
24
+            <!-- 明细情况 -->
25
+            <view class="card-header" style="padding-left: 10rpx;">明细情况</view>
26
+            <view class="card-detail">
27
+                <uni-collapse v-if="isShowBarChart1">
28
+                    <uni-collapse-item v-for="(item, index) in terminlDetail" :key="index" :title="item.terminlName"
29
+                        :open="true">
30
+                        <view class="detail-content">
31
+                            <view v-if="item.region.length > 0">在岗班组</view>
32
+                            <view class="detail-row" v-for="(regionItem, i) in item.region" :key="i">
33
+                                <view class="detail-title">{{ regionItem.regionalName }}&nbsp;&nbsp;开放数量:{{
34
+                                    regionItem.openChannelCount }}</view>
35
+                                <TableList :data="regionItem.detailList" :columns="[{
36
+                                    props: 'channelName',
37
+                                    title: '通道名称'
38
+                                }, {
39
+                                    props: 'onDutyTeamName',
40
+                                    title: '在岗班组'
41
+                                }, {
42
+                                    props: 'onDutyTime',
43
+                                    title: '上岗时间'
44
+                                }, {
45
+                                    props: 'onDutyCount',
46
+                                    title: '在岗人数'
47
+                                }]" />
48
+                            </view>
49
+                            <view v-if="item.waitList.length > 0" style="margin-bottom: 10rpx;">空闲班组</view>
50
+                            <TableList v-if="item.waitList.length > 0" :data="item.waitList" :columns="[{
51
+                                props: 'waitTeamName',
52
+                                title: '待岗班组'
53
+                            }, {
54
+                                props: 'waitCount',
55
+                                title: '待岗人数'
56
+                            }, {
57
+                                props: 'checkOutTime',
58
+                                title: '下通道时间'
59
+                            }]" />
60
+                        </view>
61
+                    </uni-collapse-item>
62
+                </uni-collapse>
63
+                <view v-else="!isShowBarChart1">
64
+                    <no-data text="暂无数据" icon-type="search" icon-size="60" icon-color="#C0C4CC" />
65
+                </view>
66
+            </view>
67
+        </view>
68
+    </home-container>
69
+</template>
70
+
71
+<script>
72
+import HomeContainer from "@/components/HomeContainer.vue";
73
+import TableList from "./components/TableList.vue";
74
+import * as echarts from 'echarts';
75
+import { getChannelStatistics } from '@/api/attendanceStatistics/attendanceStatistics';
76
+import HTabs from "@/components/h-tabs/h-tabs.vue";
77
+
78
+export default {
79
+    components: { HomeContainer, TableList, HTabs },
80
+    data() {
81
+        return {
82
+            currentTab: 'my', // 默认显示我的区域
83
+            tabList: [
84
+                {
85
+                    type: 'my',
86
+                    title: '我的区域'
87
+                },
88
+                {
89
+                    type: 'all',
90
+                    title: '全部区域'
91
+                }
92
+            ],
93
+            terminlDetail: [],
94
+            mainDetail: {},
95
+            pieChart: null,
96
+            barChart1: null,
97
+            barChart2: null,
98
+            roles: this.$store.state.user.roles
99
+        }
100
+    },
101
+    computed: {
102
+        isShowPieChart() {
103
+            const values = Object.values(this.mainDetail || {});
104
+            console.log('mainDetail values:', values, 'all empty check:', values.every(item => !item));
105
+            return !values.every(item => !item);
106
+        },
107
+        isShowBarChart1() {
108
+            return this.terminlDetail.length > 0;
109
+        },
110
+    },
111
+    mounted() {
112
+        this.fetchData("1");
113
+
114
+        if (!this.roles.includes('kezhang')) {
115
+            this.currentTab = 'all'
116
+        }
117
+    },
118
+    methods: {
119
+        handleTabChange(newValue, tab) {
120
+            this.currentTab = newValue;
121
+
122
+            this.fetchData(this.currentTab == 'my' ? "1" : "2")
123
+        },
124
+        async fetchData(status) {
125
+
126
+            try {
127
+                console.log('正在请求考勤统计数据,status:', status);
128
+                const response = await getChannelStatistics(status);
129
+                console.log('接口返回数据:', response);
130
+
131
+                const { channelCount, maintainCount, onDutyCount, terminlDetail } = response;
132
+
133
+                console.log('解析后的数据:', { channelCount, maintainCount, onDutyCount, terminlDetail });
134
+
135
+                this.terminlDetail = terminlDetail || [];
136
+                this.mainDetail = {
137
+                    channelCount: channelCount || 0,
138
+                    maintainCount: maintainCount || 0,
139
+                    onDutyCount: onDutyCount || 0,
140
+
141
+                }
142
+                console.log(this.mainDetail, 22)
143
+                this.$nextTick(() => {
144
+                    this.initCharts();
145
+                })
146
+
147
+            } catch (e) {
148
+                console.error('获取考勤数据失败', e);
149
+            }
150
+        },
151
+        initCharts() {
152
+            const pieDom = document.getElementById('pieChart');
153
+            const barDom1 = document.getElementById('barChart1');
154
+            const barDom2 = document.getElementById('barChart2');
155
+
156
+            if (pieDom) {
157
+                this.pieChart = echarts.init(pieDom);
158
+            }
159
+            if (barDom1) {
160
+                this.barChart1 = echarts.init(barDom1);
161
+            }
162
+            if (barDom2) {
163
+                this.barChart2 = echarts.init(barDom2);
164
+            }
165
+            this.$nextTick(() => {
166
+
167
+                this.updateCharts();
168
+            });
169
+        },
170
+        updateCharts() {
171
+            // 在更新图表前先清空所有图表
172
+            this.pieChart && this.pieChart.clear();
173
+            this.barChart1 && this.barChart1.clear();
174
+            this.barChart2 && this.barChart2.clear();
175
+
176
+            let openRate = 0;
177
+            let openData = 0;
178
+            let closeData = 0;
179
+            if (this.roles.includes('kezhang') && this.currentTab == 'my') {
180
+
181
+                openData = this.terminlDetail.reduce((item, cur) => {
182
+                    return item + cur.onDutyChannelCount;
183
+                }, 0);
184
+                let allData = this.terminlDetail.reduce((item, cur) => {
185
+                    return item + cur.channelCount;
186
+                }, 0);
187
+                closeData = allData - openData;
188
+                openRate = (openData / allData) * 100;
189
+            } else {
190
+                let allData = this.terminlDetail.reduce((item, cur) => {
191
+                    return item + cur.channelCount;
192
+                }, 0);
193
+
194
+                // openData = this.mainDetail.onDutyCount;
195
+                openData = this.terminlDetail.reduce((item, cur) => {
196
+                    return item + cur.onDutyChannelCount;
197
+                }, 0);
198
+                openRate = (openData / allData) * 100;
199
+                closeData = allData - openData;
200
+
201
+            }
202
+
203
+            // 检查数据是否有效
204
+            const hasValidData = openData > 0 || closeData > 0;
205
+
206
+            // 环形图配置
207
+            this.pieChart && this.pieChart.setOption({
208
+                series: [{
209
+                    type: 'pie',
210
+                    radius: ['55%', '70%'],
211
+                    avoidLabelOverlap: false,
212
+                    // label: { show: false },
213
+                    data: hasValidData ? [
214
+                        { value: openRate, name: '开放 ' + openData, itemStyle: { color: '#5972C0' } },
215
+                        { value: 100 - openRate, name: '未开放 ' + closeData, itemStyle: { color: 'rgba(89, 114, 192, 0.5)' } }
216
+                    ] : [],
217
+                    silent: !hasValidData // 无数据时禁用交互
218
+                }],
219
+                graphic: hasValidData ? [{
220
+                    type: 'text',
221
+                    left: 'center',
222
+                    top: '40%',
223
+                    style: {
224
+                        text: '开放率',
225
+                        font: 'normal 16px sans-serif',
226
+                        fill: '#333'
227
+                    }
228
+                }, {
229
+                    type: 'text',
230
+                    left: 'center',
231
+                    top: '50%',
232
+                    style: {
233
+                        text: openRate.toFixed(2) + '%',
234
+                        font: 'bold 24px sans-serif',
235
+                        fill: '#333'
236
+                    }
237
+                }] : [{
238
+                    // 无数据时显示提示
239
+                    type: 'text',
240
+                    left: 'center',
241
+                    top: 'center',
242
+                    style: {
243
+                        text: '暂无数据',
244
+                        font: '16px sans-serif',
245
+                        fill: '#999'
246
+                    }
247
+                }]
248
+            });
249
+
250
+            let firstTerminal = 'T1航站楼';
251
+            let secondTerminal = 'T2航站楼';
252
+            let terminalName = [firstTerminal, secondTerminal]
253
+
254
+            // 安全检查:确保 terminlDetail 存在且为数组
255
+            let barT1 = this.terminlDetail && Array.isArray(this.terminlDetail)
256
+                ? this.terminlDetail.filter(item => item.terminlName === firstTerminal)[0]
257
+                : null;
258
+            let barT2 = this.terminlDetail && Array.isArray(this.terminlDetail)
259
+                ? this.terminlDetail.filter(item => item.terminlName === secondTerminal)[0]
260
+                : null;
261
+
262
+            let batT1Open = barT1?.onDutyChannelCount;//T1航站楼开放人数
263
+            let batT2Open = barT2?.onDutyChannelCount;//T2航站楼开放人数
264
+            let batT1Close = barT1 ? barT1.channelCount - barT1.onDutyChannelCount : 0;//T1航站楼未开放人数
265
+            let batT2Close = barT2 ? barT2.channelCount - barT2.onDutyChannelCount : 0;//T2航站楼未开放人数
266
+            console.log(batT1Open, batT2Open, "batT2Open")
267
+
268
+            // 检查条形图数据是否有效
269
+            const hasBarData = (batT1Open > 0 || batT1Close > 0 || batT2Open > 0 || batT2Close > 0);
270
+
271
+            // 通道开放情况正负条形图
272
+            if (this.currentTab === 'all') {
273
+                this.barChart1 && this.barChart1.setOption({
274
+                    tooltip: { 
275
+                        trigger: 'axis', 
276
+                        axisPointer: { type: 'shadow' },
277
+                        formatter: function(params) {
278
+                            let result = params[0].name + '<br/>';
279
+                            params.forEach(function(item) {
280
+                                result += item.marker + ' ' + item.seriesName + ': ' + Math.abs(item.value) + '<br/>';
281
+                            });
282
+                            return result;
283
+                        }
284
+                    },
285
+                    legend: {
286
+                        data: ['开放', '未开放'],
287
+                        top: '15%'
288
+                    },
289
+                    grid: {
290
+                        left: '4%',
291
+                        right: '4%',
292
+                        bottom: '4%',
293
+                        containLabel: true,
294
+                        backgroundColor: 'transparent'
295
+                    },
296
+                    xAxis: {
297
+                        type: 'value',
298
+                        axisLine: { show: true },
299
+                        axisTick: { show: false },
300
+                        axisLabel: {
301
+                            show: false,
302
+                            formatter: function (value) {
303
+                                return Math.abs(value);
304
+                            }
305
+                        },
306
+                        splitLine: { show: false },
307
+                        min: function (value) {
308
+                            return value.min - (value.max - value.min) * 0.3;
309
+                        },
310
+                        max: function (value) {
311
+                            return value.max + (value.max - value.min) * 0.3;
312
+                        }
313
+                    },
314
+                    yAxis: {
315
+                        type: 'category',
316
+                        axisLine: { show: false },
317
+                        axisTick: { show: false },
318
+                        data: terminalName,
319
+                        splitLine: { show: false }
320
+                    },
321
+                    series: hasBarData ? [
322
+                        {
323
+                            name: '开放',
324
+                            type: 'bar',
325
+                            data: [-batT1Open, -batT2Open],
326
+                            stack: 'total',
327
+                            barWidth: '30%',
328
+                            itemStyle: { color: '#6E65F1' },
329
+                            label: {
330
+                                show: function (params) {
331
+                                    return Math.abs(params.value) !== 0;
332
+                                },
333
+                                position: 'left',
334
+                                formatter: function (params) {
335
+                                    return Math.abs(params.value) === 0 ? '' : Math.abs(params.value);
336
+                                }
337
+                            }
338
+                        },
339
+                        {
340
+                            name: '未开放',
341
+                            type: 'bar',
342
+                            stack: 'total',
343
+                            barWidth: '30%',
344
+                            data: [batT1Close, batT2Close],
345
+                            itemStyle: { color: '#D4D1FB' },
346
+                            label: {
347
+                                show: function (params) {
348
+                                    return Math.abs(params.value) !== 0;
349
+                                },
350
+                                position: 'right',
351
+                                formatter: function (params) {
352
+                                    return Math.abs(params.value) === 0 ? '' : Math.abs(params.value);
353
+                                }
354
+                            }
355
+                        }
356
+                    ] : [],
357
+                    graphic: !hasBarData ? [{
358
+                        // 无数据时显示提示
359
+                        type: 'text',
360
+                        left: 'center',
361
+                        top: 'center',
362
+                        style: {
363
+                            text: '暂无数据',
364
+                            font: '16px sans-serif',
365
+                            fill: '#999'
366
+                        }
367
+                    }] : []
368
+                });
369
+            }
370
+
371
+
372
+            let batT1NormalPerson = barT1?.onDutyCount;//T1航站楼在岗人数
373
+            let batT2NormalPerson = barT2?.onDutyCount;//T2航站楼在岗人数
374
+            let batT1FreePerson = barT1 ? barT1.openChannelCount - barT1.onDutyCount : 0;//T1航站楼空闲人数
375
+            let batT2FreePerson = barT2 ? barT2.openChannelCount - barT2.onDutyCount : 0;//T2航站楼空闲人数
376
+            console.log(batT1NormalPerson, batT2NormalPerson, batT1FreePerson, batT2FreePerson, "batT2NormalPerson")
377
+            // 检查条形图数据是否有效
378
+            const hasBar2Data = (batT1NormalPerson > 0 || batT2NormalPerson > 0 || batT1FreePerson > 0 || batT2FreePerson > 0);
379
+            // 人员情况正负条形图
380
+            if (this.currentTab === 'all') {
381
+                this.barChart2 && this.barChart2.setOption({
382
+                    tooltip: { 
383
+                        trigger: 'axis', 
384
+                        axisPointer: { type: 'shadow' },
385
+                        formatter: function(params) {
386
+                            let result = params[0].name + '<br/>';
387
+                            params.forEach(function(item) {
388
+                                result += item.marker + ' ' + item.seriesName + ': ' + Math.abs(item.value) + '<br/>';
389
+                            });
390
+                            return result;
391
+                        }
392
+                    },
393
+                    legend: { data: ['在岗人数', '空闲人数'], top: '15%' },
394
+                    grid: {
395
+                        left: '4%',
396
+                        right: '4%',
397
+                        bottom: '4%',
398
+                        containLabel: true,
399
+                        backgroundColor: 'transparent'
400
+                    },
401
+                    xAxis: {
402
+                        type: 'value',
403
+                        axisLine: { show: true },
404
+                        axisTick: { show: false },
405
+                        axisLabel: {
406
+                            show: false,
407
+                            formatter: function (value) {
408
+                                return Math.abs(value);
409
+                            }
410
+                        },
411
+                        splitLine: { show: false },
412
+                        min: function (value) {
413
+                            return value.min - (value.max - value.min) * 0.3;
414
+                        },
415
+                        max: function (value) {
416
+                            return value.max + (value.max - value.min) * 0.3;
417
+                        }
418
+                    },
419
+                    yAxis: {
420
+                        type: 'category',
421
+                        axisLine: { show: false },
422
+                        axisTick: { show: false },
423
+                        data: terminalName,
424
+                        splitLine: { show: false }
425
+                    },
426
+                    series: hasBar2Data ? [
427
+                        {
428
+                            name: '在岗人数',
429
+                            type: 'bar',
430
+                            data: [-batT1NormalPerson, -batT2NormalPerson],
431
+                            stack: 'total',
432
+                            barWidth: '30%',
433
+                            itemStyle: { color: '#6E65F1' },
434
+                            label: {
435
+                                show: true,
436
+                                position: 'left',
437
+                                formatter: function (params) {
438
+                                    return Math.abs(params.value) === 0 ? '' : Math.abs(params.value);
439
+                                }
440
+                            }
441
+                        },
442
+                        {
443
+                            name: '空闲人数',
444
+                            type: 'bar',
445
+                            stack: 'total',
446
+                            barWidth: '30%',
447
+                            data: [batT1FreePerson, batT2FreePerson],
448
+                            itemStyle: { color: '#D4D1FB' },
449
+                            label: {
450
+                                show: true,
451
+                                position: 'right',
452
+                                formatter: function (params) {
453
+                                    return Math.abs(params.value) === 0 ? '' : Math.abs(params.value);
454
+                                }
455
+                            }
456
+                        }
457
+                    ] : [],
458
+                    graphic: !hasBar2Data ? [{
459
+                        // 无数据时显示提示
460
+                        type: 'text',
461
+                        left: 'center',
462
+                        top: 'center',
463
+                        style: {
464
+                            text: '暂无数据',
465
+                            font: '16px sans-serif',
466
+                            fill: '#999'
467
+                        }
468
+                    }] : []
469
+                });
470
+            }
471
+
472
+
473
+        }
474
+    }
475
+}
476
+</script>
477
+
478
+<style lang="scss" scoped>
479
+.container {
480
+    padding: 20rpx;
481
+    display: flex;
482
+    flex-direction: column;
483
+    gap: 20rpx;
484
+    background: none !important;
485
+}
486
+
487
+.card-detail {
488
+    padding: 0;
489
+    background: #fff;
490
+    border-radius: 16rpx;
491
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
492
+}
493
+
494
+.card {
495
+    background: #fff;
496
+    border-radius: 16rpx;
497
+    padding: 24rpx;
498
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
499
+}
500
+
501
+.card-header {
502
+    font-size: 32rpx;
503
+    font-weight: bold;
504
+    margin-bottom: 20rpx;
505
+    color: #333;
506
+}
507
+
508
+.chart-container {
509
+    position: relative;
510
+    height: 300rpx;
511
+    margin-bottom: 20rpx;
512
+}
513
+
514
+.pie-chart,
515
+.bar-chart {
516
+    width: 100% !important;
517
+    height: 400rpx;
518
+}
519
+
520
+
521
+
522
+
523
+.open-rate {
524
+    position: absolute;
525
+    top: 50%;
526
+    left: 50%;
527
+    transform: translate(-50%, -50%);
528
+    font-size: 28rpx;
529
+    color: #666;
530
+}
531
+
532
+.detail-content {
533
+    padding: 20rpx;
534
+}
535
+
536
+.detail-row {
537
+    display: flex;
538
+    justify-content: space-between;
539
+    margin-bottom: 10rpx;
540
+    font-size: 26rpx;
541
+    color: #666;
542
+    flex-direction: column;
543
+
544
+    .detail-title {
545
+        margin: 10rpx 0;
546
+        color: #6E65F1;
547
+    }
548
+}
549
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1460 - 0
src/pages/capabilityComparison/index.vue


+ 0 - 0
src/pages/capabilityComparison/能力对比


+ 315 - 0
src/pages/checklist/components/SelectPerson.vue

@@ -0,0 +1,315 @@
1
+<template>
2
+  <view class="select-person-modal">
3
+    <view class="slot-content" @click.stop="openModal">
4
+      <slot></slot>
5
+    </view>
6
+    <u-popup :show="show" mode="center" :round="8" @close="close">
7
+      <view class="modal-content">
8
+        <view class="title">
9
+          <text>选择人员</text>
10
+          <u-icon name="close" color="#666666" size="20" @click="close" />
11
+        </view>
12
+
13
+        <view class="title-cell">
14
+          <SelectData ref="selectData" @search="searchUsers" @searchViewShowEvent="searchViewShowHandler" />
15
+        </view>
16
+
17
+        <view class="title-cell" v-if="!searchViewShow && selectedUsers && selectedUsers.length">
18
+          <view class="title-text">{{ `已选人员(${selectedUsers.length}人)` }}</view>
19
+          <list-user :data="selectedUsers" :show-delete="true" @delete="handleRemoveSelectedUser" />
20
+        </view>
21
+
22
+        <view class="title-cell" v-if="searchViewShow">
23
+          <view style="margin-bottom: 15px;" v-if="addUsers && addUsers.length">
24
+            <view class="title-text">
25
+              {{ `添加人员(${addUsers.length}人)` }}
26
+            </view>
27
+            <list-user :data="addUsers" :show-delete="true" @delete="handleRemoveAddUser" />
28
+          </view>
29
+
30
+          <view class="title-text">
31
+            {{ `搜索结果(${searchResults.length}人)` }}
32
+          </view>
33
+          <list-user v-if="searchResults && searchResults.length" :data="searchResults" :show-delete="false"
34
+            @delete="handleRemoveSearchResult" @click="handleSearchResultClick" />
35
+          <view v-else class="empty">
36
+            <text v-if="searchKeyword">暂无搜索结果</text>
37
+            <text v-else>请输入关键词搜索</text>
38
+          </view>
39
+        </view>
40
+
41
+        <view class="footer-btn">
42
+          <view v-if="searchViewShow" class="custom-btn-normal" @click="appendUsers"
43
+            :class="{ disabled: addUsers.length === 0 }">添加人员</view>
44
+          <view v-else class="custom-btn-normal" @click="confirmSelection"
45
+            :class="{ disabled: selectedUsers.length === 0 }">确认选择</view>
46
+        </view>
47
+      </view>
48
+    </u-popup>
49
+  </view>
50
+</template>
51
+
52
+<script>
53
+import SelectData from '@/pages/attendance/components/SelectData'
54
+import UserAvatar from '@/pages/attendance/components/UserAvatar'
55
+import { formatName } from '@/utils/formatUtils'
56
+import {
57
+  getUserList,
58
+} from "@/api/attendance/attendance"
59
+export default {
60
+  components: { SelectData, UserAvatar },
61
+  props: {
62
+    // 初始选中的人员列表
63
+    value: {
64
+      type: Array,
65
+      default: () => []
66
+    },
67
+    // 是否多选
68
+    multiple: {
69
+      type: Boolean,
70
+      default: true
71
+    },
72
+    // 占位符文本
73
+    placeholder: {
74
+      type: String,
75
+      default: '点击选择人员'
76
+    },
77
+    // 是否禁用
78
+    disabled: {
79
+      type: Boolean,
80
+      default: false
81
+    }
82
+  },
83
+  data() {
84
+    return {
85
+      show: false,
86
+      selectedUsers: [], // 选中的人员
87
+      searchResults: [], // 搜索结果
88
+      addUsers: [], // 待添加的人员
89
+      searchViewShow: false,
90
+      searchKeyword: ''
91
+    }
92
+  },
93
+  watch: {
94
+    value: {
95
+      handler(newValue) {
96
+        this.selectedUsers = [...newValue]
97
+      },
98
+      immediate: true
99
+    },
100
+    show(newValue) {
101
+      if (newValue) {
102
+        this.searchViewShow = false
103
+        this.searchResults = []
104
+        this.addUsers = []
105
+        this.searchKeyword = ''
106
+        if (this.$refs.selectData) {
107
+          this.$refs.selectData.clearInput()
108
+        }
109
+      }
110
+    }
111
+  },
112
+  methods: {
113
+    openModal() {
114
+      if (this.disabled) return
115
+      this.show = true
116
+    },
117
+    close() {
118
+      this.show = false
119
+    },
120
+
121
+    searchViewShowHandler(show) {
122
+      this.searchViewShow = show || this.addUsers.length
123
+    },
124
+
125
+    // 搜索用户
126
+    async searchUsers(keyword) {
127
+   
128
+      this.searchKeyword = keyword.trim()
129
+      if (!this.searchKeyword) {
130
+        this.searchResults = []
131
+        return
132
+      }
133
+
134
+      try {
135
+        const response = await getUserList({
136
+          nickName: this.searchKeyword,
137
+          status: '0'
138
+        })
139
+        
140
+
141
+        if (response && response.code === 200) {
142
+          this.searchResults = (response.rows || []).map(item => ({
143
+            ...item,
144
+            nickName: formatName(item.nickName),
145
+            // 确保包含部门信息
146
+            deptName: item.dept?.deptName || item.deptName || ''
147
+          }))
148
+        } else {
149
+          this.searchResults = []
150
+          uni.showToast({
151
+            title: '搜索失败,请重试',
152
+            icon: 'none',
153
+            duration: 2000
154
+          })
155
+        }
156
+      } catch (error) {
157
+        this.searchResults = []
158
+        uni.showToast({
159
+          title: '搜索失败,请重试',
160
+          icon: 'none',
161
+          duration: 2000
162
+        })
163
+      }
164
+    },
165
+
166
+    // 选择待添加人员
167
+    selectAddUser(user) {
168
+      const index = this.addUsers.findIndex(item => item.userId === user.userId)
169
+      if (index >= 0) {
170
+        this.addUsers.splice(index, 1)
171
+      } else {
172
+        this.addUsers.push(user)
173
+      }
174
+    },
175
+
176
+    // 添加人员到选中列表
177
+    appendUsers() {
178
+      if (this.addUsers.length) {
179
+        this.addUsers.forEach(user => {
180
+          // 确保添加的人员数据包含部门信息
181
+          const userWithDept = {
182
+            ...user,
183
+            dept_name: user.dept?.deptName || user.deptName || ''
184
+          };
185
+          this.selectUser(userWithDept, false)
186
+        })
187
+        this.addUsers = []
188
+        this.searchViewShow = false
189
+        if (this.$refs.selectData) {
190
+          this.$refs.selectData.clearInput()
191
+        }
192
+      }
193
+    },
194
+
195
+    // 移除选中人员
196
+    removeSelectedUser(user, onlyCheck = false) {
197
+      const index = this.selectedUsers.findIndex(item => item.userId === user.userId)
198
+      if (index >= 0) {
199
+        if (!onlyCheck) {
200
+          this.selectedUsers.splice(index, 1)
201
+        }
202
+        return undefined
203
+      }
204
+      return user
205
+    },
206
+
207
+    // 处理删除已选人员
208
+    handleRemoveSelectedUser({ deletedItem, index, newData }) {
209
+      this.selectedUsers = newData
210
+    },
211
+
212
+    // 处理删除待添加人员
213
+    handleRemoveAddUser({ deletedItem, index, newData }) {
214
+      this.addUsers = newData
215
+    },
216
+
217
+    // 处理删除搜索结果(虽然不应该有删除操作,但为了完整性)
218
+    handleRemoveSearchResult({ deletedItem, index, newData }) {
219
+      this.searchResults = newData
220
+    },
221
+
222
+    // 选择人员处理
223
+    selectUser(user, unique = true) {
224
+      const addItem = this.removeSelectedUser(user, !unique)
225
+      if (addItem) {
226
+        this.selectedUsers.push(addItem)
227
+      }
228
+    },
229
+
230
+    // 确认选择
231
+    confirmSelection() {
232
+      if (this.selectedUsers.length === 0) {
233
+        uni.showToast({
234
+          title: '请选择至少一位人员',
235
+          icon: 'none'
236
+        })
237
+        return
238
+      }
239
+
240
+    
241
+      this.$emit('change', this.selectedUsers)
242
+      this.$emit('confirm', this.selectedUsers)
243
+
244
+      this.show = false
245
+
246
+      uni.showToast({
247
+        title: `已选择${this.selectedUsers.length}人`,
248
+        icon: 'success',
249
+        duration: 1500
250
+      })
251
+    },
252
+
253
+    // 处理搜索结果点击
254
+    handleSearchResultClick({ clickedItem, index, data }) {
255
+      if (clickedItem) {
256
+        this.selectAddUser(clickedItem)
257
+      }
258
+    },
259
+  }
260
+}
261
+</script>
262
+
263
+<style lang="scss" scoped>
264
+.select-person-modal {
265
+  .slot-content {
266
+    display: inline-block;
267
+  }
268
+
269
+  .empty {
270
+    text-align: center;
271
+    padding: 20px;
272
+    color: #999;
273
+    font-size: 14px;
274
+  }
275
+
276
+  .modal-content {
277
+    width: 90vw;
278
+    max-height: 75vh;
279
+    padding: 10px 0;
280
+    overflow-y: auto;
281
+
282
+    .title {
283
+      height: 24px;
284
+      font-weight: 400;
285
+      font-size: 18px;
286
+      color: #333333;
287
+      line-height: 21px;
288
+      display: flex;
289
+      justify-content: space-between;
290
+      align-items: center;
291
+      padding: 15px;
292
+      box-sizing: border-box;
293
+      border-bottom: 1px solid #f0f0f0;
294
+    }
295
+
296
+    .title-cell {
297
+      padding: 15px;
298
+
299
+      .title-text {
300
+        font-size: 16px;
301
+        font-weight: 500;
302
+        color: #333;
303
+        margin-bottom: 12px;
304
+      }
305
+    }
306
+
307
+    .footer-btn {
308
+      padding: 15px;
309
+      border-top: 1px solid #f0f0f0;
310
+
311
+
312
+    }
313
+  }
314
+}
315
+</style>

+ 121 - 0
src/pages/checklist/components/SubmitResult.vue

@@ -0,0 +1,121 @@
1
+<template>
2
+    <u-popup ref="popup" :show="showPopup" mode="center" :mask-close-able="false" :round="16">
3
+        <view class="submit-result-modal">
4
+            <!-- 成功图片 -->
5
+            <image src="/static/images/submitOK.png" class="success-image" mode="aspectFit" />
6
+
7
+            <!-- 成功提示文字 -->
8
+            <text class="success-text">提交成功</text>
9
+
10
+            <!-- 按钮区域 -->
11
+            <view class="button-group">
12
+                <view class="custom-btn-white" @click="handleBackToList">
13
+                    返回列表
14
+                </view>
15
+                <view class="custom-btn-normal" @click="handleContinueSubmit">
16
+                    继续提交
17
+                </view>
18
+            </view>
19
+        </view>
20
+    </u-popup>
21
+</template>
22
+
23
+<script>
24
+export default {
25
+    name: 'SubmitResult',
26
+    data() {
27
+        return {
28
+            showPopup: false
29
+        };
30
+    },
31
+    methods: {
32
+        // 打开模态框
33
+        open() {
34
+            this.showPopup = true;
35
+        },
36
+
37
+        // 关闭模态框
38
+        close() {
39
+            this.showPopup = false;
40
+        },
41
+
42
+        // 返回列表
43
+        handleBackToList() {
44
+            this.close();
45
+           
46
+            uni.navigateBack({ delta: 1 });
47
+        },
48
+
49
+        // 继续提交
50
+        handleContinueSubmit() {
51
+            this.close();
52
+            // 触发继续提交事件
53
+            this.$emit('continue-submit');
54
+        }
55
+    }
56
+}
57
+</script>
58
+
59
+<style scoped>
60
+.submit-result-modal {
61
+    background: #ffffff;
62
+    border-radius: 16rpx;
63
+    padding: 60rpx 40rpx 40rpx;
64
+    width: 560rpx;
65
+    display: flex;
66
+    flex-direction: column;
67
+    align-items: center;
68
+}
69
+
70
+.success-image {
71
+    width: 400rpx;
72
+    height: 400rpx;
73
+    margin-bottom: 32rpx;
74
+}
75
+
76
+.success-text {
77
+    font-size: 36rpx;
78
+    font-weight: 600;
79
+    color: #333333;
80
+    margin-bottom: 48rpx;
81
+    text-align: center;
82
+}
83
+
84
+.button-group {
85
+    display: flex;
86
+    justify-content: space-between;
87
+    width: 100%;
88
+    gap: 20rpx;
89
+}
90
+
91
+.btn {
92
+    flex: 1;
93
+    height: 80rpx;
94
+    border-radius: 12rpx;
95
+    font-size: 28rpx;
96
+    font-weight: 500;
97
+    border: none;
98
+    display: flex;
99
+    align-items: center;
100
+    justify-content: center;
101
+}
102
+
103
+.btn-primary {
104
+    background: #2979FF;
105
+    color: #ffffff;
106
+}
107
+
108
+.btn-primary:active {
109
+    background: #1E62D9;
110
+}
111
+
112
+.btn-secondary {
113
+    background: #F2F2F2;
114
+    color: #666666;
115
+    border: 1px solid #E0E0E0;
116
+}
117
+
118
+.btn-secondary:active {
119
+    background: #E8E8E8;
120
+}
121
+</style>

+ 216 - 0
src/pages/checklist/components/unqualified.vue

@@ -0,0 +1,216 @@
1
+<template>
2
+  <view class="unqualified-container">
3
+    <!-- 头部标题和加号 -->
4
+    <template v-if="formData.checkedLevel !== 'PERSONNEL_LEVEL'">
5
+      <view class="header">
6
+        <text class="title">不合格人员</text>
7
+        <!-- 人员选择组件,加号放在插槽中 -->
8
+        <SelectPerson ref="selectPerson" :value="localPersonnelData" @input="handlePersonnelSelect" :disabled="disabled"
9
+          @confirm="handlePersonnelConfirm">
10
+          <uni-icons type="plus" size="24" :color="disabled ? '#999' : '#409EFF'" class="add-icon" />
11
+        </SelectPerson>
12
+      </view>
13
+
14
+      <!-- 人员列表 -->
15
+      <view class="personnel-list" v-if="localPersonnelData && localPersonnelData.length > 0">
16
+        <list-user :data="localPersonnelData" :show-delete="showDelete" @delete="handlePersonnelDelete"
17
+          :disabled="disabled" />
18
+      </view>
19
+
20
+      <view class="empty-tip" v-else>
21
+        <text>暂无不合格人员</text>
22
+      </view>
23
+    </template>
24
+
25
+    <!-- 问题描述输入框 -->
26
+    <view class="problem-input">
27
+      <uni-easyinput type="textarea" :placeholder="placeholder" v-model="problemDescription" @input="handleProblemInput"
28
+        :maxlength="-1" :autoHeight="true" :disabled="disabled" />
29
+    </view>
30
+  </view>
31
+</template>
32
+
33
+<script>
34
+import ListUser from '@/components/list-user/list-user.vue'
35
+import uniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
36
+import uniEasyinput from '@/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue'
37
+import SelectPerson from './SelectPerson.vue'
38
+
39
+export default {
40
+  name: 'UnqualifiedPersonnel',
41
+  components: {
42
+    ListUser,
43
+    uniIcons,
44
+    uniEasyinput,
45
+    SelectPerson
46
+  },
47
+  props: {
48
+    formData: {
49
+      type: Object,
50
+      default: () => { }
51
+    },
52
+    roles: {
53
+      type: Array,
54
+      default: () => []
55
+    },
56
+    // 人员数据
57
+    personnelData: {
58
+      type: Array,
59
+      default: () => []
60
+    },
61
+    // 是否显示删除按钮
62
+    showDelete: {
63
+      type: Boolean,
64
+      default: true
65
+    },
66
+    // 问题描述
67
+    value: {
68
+      type: String,
69
+      default: ''
70
+    },
71
+    // 输入框占位符
72
+    placeholder: {
73
+      type: String,
74
+      default: '请输入问题描述'
75
+    },
76
+    // 父项目索引
77
+    parentIndex: {
78
+      type: Number,
79
+      default: -1
80
+    },
81
+    // 子项目索引
82
+    childIndex: {
83
+      type: Number,
84
+      default: -1
85
+    },
86
+    // 禁用状态
87
+    disabled: {
88
+      type: Boolean,
89
+      default: false
90
+    }
91
+  },
92
+  data() {
93
+    return {
94
+      problemDescription: this.value,
95
+      localPersonnelData: [...this.personnelData] // 创建本地副本
96
+    }
97
+  },
98
+  watch: {
99
+    value(newVal) {
100
+      this.problemDescription = newVal
101
+    },
102
+    personnelData(newVal) {
103
+      // 当父组件更新personnelData时,同步更新本地数据
104
+      this.localPersonnelData = [...newVal]
105
+    }
106
+  },
107
+  methods: {
108
+    // 处理人员选择
109
+    handlePersonnelSelect(selectedUsers) {
110
+      this.localPersonnelData = [...selectedUsers]
111
+
112
+      this.emitPersonnelChange()
113
+    },
114
+
115
+    // 回显 不合格人员
116
+    handlePersonnelConfirm(selectedUsers) {
117
+      this.localPersonnelData = [...selectedUsers]
118
+      this.emitPersonnelChange()
119
+    },
120
+
121
+
122
+
123
+    // 处理人员删除
124
+    handlePersonnelDelete({ item, index, newData }) {
125
+      this.localPersonnelData.splice(index, 1)
126
+
127
+      this.emitPersonnelChange()
128
+    },
129
+
130
+    // 处理问题描述输入
131
+    handleProblemInput(value) {
132
+      this.problemDescription = value
133
+
134
+      this.emitPersonnelChange()
135
+    },
136
+
137
+    // 向外传递完整的对象
138
+    emitPersonnelChange() {
139
+      let personnelData = this.localPersonnelData.map(item => ({
140
+        ...item,
141
+        userName:item.nickName.replace(/\s+/g, ''),
142
+        nickName:item.nickName.replace(/\s+/g, ''), // 去掉nickName中的所有空格
143
+      }))
144
+      const changeData = {
145
+        parentIndex: this.parentIndex,
146
+        childIndex: this.childIndex,
147
+        personnelData: [...personnelData],
148
+        problemDescription: this.problemDescription
149
+      }
150
+      this.$emit('personnel-change', changeData)
151
+    }
152
+  }
153
+}
154
+</script>
155
+
156
+<style lang="scss" scoped>
157
+.unqualified-container {
158
+  background: #F5F5F5;
159
+  border-radius: 12rpx;
160
+  padding: 32rpx;
161
+  margin: 24rpx 0;
162
+}
163
+
164
+.header {
165
+  display: flex;
166
+  justify-content: space-between;
167
+  align-items: center;
168
+  margin-bottom: 24rpx;
169
+
170
+  .title {
171
+    font-size: 32rpx;
172
+    font-weight: 600;
173
+    color: #333333;
174
+    line-height: 44rpx;
175
+  }
176
+
177
+  .add-icon {
178
+    padding: 8rpx;
179
+    background: #FFFFFF;
180
+    border-radius: 50%;
181
+    box-shadow: 0 4rpx 12rpx rgba(64, 158, 255, 0.3);
182
+    cursor: pointer;
183
+  }
184
+}
185
+
186
+.personnel-list {
187
+  margin-bottom: 24rpx;
188
+}
189
+
190
+.empty-tip {
191
+  text-align: center;
192
+  padding: 48rpx 0;
193
+  color: #999999;
194
+  font-size: 28rpx;
195
+}
196
+
197
+.problem-input {
198
+  ::v-deep .uni-easyinput__content {
199
+    background: #FFFFFF;
200
+    border-radius: 12rpx;
201
+    padding: 24rpx;
202
+    min-height: 120rpx;
203
+  }
204
+
205
+  ::v-deep .uni-textarea-textarea {
206
+    font-size: 28rpx;
207
+    line-height: 40rpx;
208
+    color: #333333;
209
+  }
210
+
211
+  ::v-deep .uni-easyinput__placeholder {
212
+    color: #999999;
213
+    font-size: 28rpx;
214
+  }
215
+}
216
+</style>

+ 986 - 0
src/pages/checklist/index.vue

@@ -0,0 +1,986 @@
1
+<template>
2
+    <home-container>
3
+        <view class="report-container">
4
+            <!-- 表单区域 -->
5
+            <uni-forms ref="form" :rules="rules" :modelValue="formData" label-position="top" err-show-type="modal">
6
+                <!-- 基本信息分组 (默认展开) -->
7
+                <view class="card">
8
+                    <uni-collapse :accordion="false" :value="['group1']">
9
+                        <h-collapse-item name="group1" :show-animation="true"
10
+                            :iconUrl="'../../static/images/icon/jiben.png'" :title="'基本信息'">
11
+                            <uni-forms-item label="任务编号" name="taskCode">
12
+                                <uni-easyinput v-model="formData.taskCode" placeholder="系统自动生成"
13
+                                    :disabled="true"></uni-easyinput>
14
+                            </uni-forms-item>
15
+                            <uni-forms-item label="检查人" name="checkerName">
16
+                                <uni-easyinput v-model="formData.checkerName" placeholder="请输入检查人姓名" :disabled="true" />
17
+                            </uni-forms-item>
18
+                            <uni-forms-item label="执行频率" name="frequency">
19
+                                <uni-easyinput :value="`${formData.ruleTypeDesc}${formData.ruleTypeNum}次`"
20
+                                    placeholder="请输入执行频率" :disabled="true" />
21
+                            </uni-forms-item>
22
+                            <uni-forms-item label="任务简介" name="description">
23
+                                <uni-easyinput type="textarea" v-model="formData.description" placeholder="请输入任务简介"
24
+                                    :disabled="true" />
25
+                            </uni-forms-item>
26
+                            <uni-forms-item :label="getCheckLabel" :name="getCheckFieldName" required>
27
+                                <uni-data-picker v-if="getCheckLabel == '被检查主管'" :localdata="departments"
28
+                                    :popup-title="`请选择${getCheckLabel}`" v-model="formData.checkedDepartmentId"
29
+                                    @change="handlecheckedDepartmentIdChange" :readonly="formDisabled" />
30
+                                <uni-data-picker v-if="getCheckLabel == '被检查大队'" :localdata="brigades"
31
+                                    :popup-title="`请选择${getCheckLabel}`" v-model="formData.checkedBrigadeId"
32
+                                    @change="handlecheckedBrigadeIdChange" :readonly="formDisabled" />
33
+                                <uni-data-picker v-if="getCheckLabel == '被检查班组'" :localdata="teams"
34
+                                    :popup-title="`请选择${getCheckLabel}`" v-model="formData.checkedTeamId"
35
+                                    @change="handlecheckedTeamIdChange" :readonly="formDisabled" />
36
+
37
+                                <fuzzy-select v-if="getCheckLabel == '被检查人'" v-model="formData.checkedPersonnelId"
38
+                                    :options="userOptions" placeholder="请输入被检查人姓名搜索" data-value="userId"
39
+                                    data-text="nickName" @change="handleCheckedSelect" :disabled="formDisabled" />
40
+                            </uni-forms-item>
41
+                            <uni-forms-item label="检查地点" name="checkLocation" required>
42
+                                <uni-data-picker :localdata="location_options" popup-title="请选择检查地点"
43
+                                    v-model="formData.checkLocation" @change="handleCheckLocationChange"
44
+                                    :readonly="formDisabled" />
45
+                            </uni-forms-item>
46
+                            <uni-forms-item label="检查时间" name="checkTime" required>
47
+                                <uni-datetime-picker type="datetime" :start="startDate" v-model="formData.checkTime"
48
+                                    :disabled="formDisabled" />
49
+                            </uni-forms-item>
50
+                        </h-collapse-item>
51
+                    </uni-collapse>
52
+                </view>
53
+
54
+                <!-- 巡检项目分组 -->
55
+                <view class="card">
56
+                    <uni-collapse :accordion="false" :value="['group2']">
57
+                        <h-collapse-item title="巡检项目" name="group2" :show-animation="true"
58
+                            :iconUrl="'../../static/images/icon/xunjian.png'" ref="xunjianCollapseItem">
59
+                            <view style="padding: 0 15px 15px 15px;">
60
+                                <!-- 巡检项目描述卡片 -->
61
+                                <list-card v-for="(item, index) in formData.checkProjectItemList" :key="item.id">
62
+                                    <template #title>
63
+                                        <view class="item-title">
64
+                                            {{ item.categoryNameOne }}/{{ item.categoryNameTwo }}
65
+                                        </view>
66
+                                    </template>
67
+                                    <view v-for="(ele, i) in item.children" :key="ele.id">
68
+                                        <view class="list-row-space-between">
69
+                                            <view class="list-label">
70
+                                                {{ ele.projectName }}
71
+                                            </view>
72
+                                            <view class="list-value">
73
+                                                <TextSwitch class="text-switch" v-model="ele.scoreLevel"
74
+                                                    uncheck_text="不符合" uncheck_value="UNQUALIFIED" checked_text="符合"
75
+                                                    checked_value="QUALIFIED" :disabled="formDisabled" />
76
+                                            </view>
77
+                                        </view>
78
+                                        <!-- 不合格人员组件 -->
79
+                                        <view v-if="ele.scoreLevel == 'UNQUALIFIED'">
80
+                                            <unqualified-personnel v-model="ele.problemDescription" :formData="formData"
81
+                                                :personnel-data="ele.checkUserList || []" :parent-index="index"
82
+                                                :child-index="i" @personnel-change="handlePersonnelChange"
83
+                                                :disabled="formDisabled" />
84
+                                        </view>
85
+                                    </view>
86
+                                </list-card>
87
+
88
+
89
+                                <!-- 不合格图片上传 -->
90
+                                <view class="upload-section" v-if="!allItemsQualified">
91
+                                    <text class="upload-title">上传不合格图片</text>
92
+                                    <uni-file-picker v-model="formData.baseAttachmentList" limit="8" title="最多上传8张"
93
+                                        style="height: 350rpx;" :image-styles="imageStyles" fileMediatype="image"
94
+                                        mode="grid" @select="onSelect" :disabled="formDisabled"
95
+                                        :readonly="formDisabled" />
96
+                                </view>
97
+                            </view>
98
+                        </h-collapse-item>
99
+                    </uni-collapse>
100
+                </view>
101
+
102
+                <!-- 整改要求分组 -->
103
+                <view class="card" v-if="rectificationSuggestionsRequired">
104
+                    <uni-collapse :accordion="false" :value="['group3']">
105
+                        <h-collapse-item title="整改要求" name="group3" :show-animation="true"
106
+                            :iconUrl="'../../static/images/icon/zhenggai.png'">
107
+                            <uni-forms-item label="责任人" name="responsibleUserId" required>
108
+                                <fuzzy-select v-model="formData.responsibleUserId" :options="userOptions"
109
+                                    placeholder="请输入责任人姓名搜索" data-value="userId" data-text="nickName"
110
+                                    @change="handleUserSelect" :disabled="formDisabled" />
111
+                            </uni-forms-item>
112
+
113
+                            <uni-forms-item label="整改期限" name="rectificationDeadline" required>
114
+                                <uni-datetime-picker type="datetime" :start="startDate"
115
+                                    v-model="formData.rectificationDeadline" :disabled="formDisabled" />
116
+                            </uni-forms-item>
117
+
118
+                            <uni-forms-item label="整改要求" name="rectificationSuggestions" required>
119
+                                <uni-easyinput type="textarea" v-model="formData.rectificationSuggestions"
120
+                                    placeholder="请输入整改要求" :disabled="formDisabled" />
121
+                            </uni-forms-item>
122
+                        </h-collapse-item>
123
+                    </uni-collapse>
124
+                </view>
125
+
126
+                <!-- 提交按钮 -->
127
+                <view class="button-group" v-if="formData.checkRecordStatus == 'DRAFT' || type == 'task'">
128
+                    <view class="custom-btn-white" @click="saveDraft">保存草稿</view>
129
+                    <view class="custom-btn-normal" @click="submitForm">立刻提交</view>
130
+                </view>
131
+            </uni-forms>
132
+        </view>
133
+
134
+        <SubmitResult ref="submitResult" @continue-submit="handleContinueSubmit" />
135
+    </home-container>
136
+</template>
137
+
138
+<script>
139
+import HomeContainer from "@/components/HomeContainer.vue";
140
+import ListCard from "@/components/list-card/list-card.vue";
141
+import { getCheckTask } from "@/api/check/checkTask.js"
142
+import TextSwitch from "@/components/text-switch/text-switch.vue"
143
+import { checkedLevelEnums } from "@/utils/enums.js"
144
+import UnqualifiedPersonnel from "./components/unqualified.vue"
145
+import { treeSelectByType } from "@/api/system/common"
146
+import { buildTeamOptions, buildDepartmentOptions, buildBrigadeOptions, buildManagerOptions } from '@/utils/common'
147
+import useDictMixin from '@/utils/dict'
148
+import { getDeptRoleUser } from "@/api/check/checklist.js"
149
+import { addDraftInspection, submitInspection, getInspectionListById } from '@/api/check/checkReward.js'
150
+import SubmitResult from './components/SubmitResult.vue'
151
+import { selectDeptLeaderByUserId } from '@/api/approve/approve.js'
152
+import config from '@/config'
153
+import { getToken } from '@/utils/auth'
154
+import { listAllUser } from "@/api/system/user.js"
155
+import { formatTime } from '@/utils/formatUtils'
156
+import { getDeptList, getDeptManager } from "@/api/system/dept/dept.js"
157
+import FuzzySelect from "@/components/fuzzy-select/fuzzy-select.vue"
158
+export default {
159
+    components: { HomeContainer, ListCard, TextSwitch, UnqualifiedPersonnel, FuzzySelect, SubmitResult },
160
+    mixins: [useDictMixin],
161
+    computed: {
162
+        currentUser() {
163
+            return this.$store.state.user;
164
+        },
165
+        notkezhang() {
166
+            let roles = this.$store.state.user.roles
167
+            return !roles || !roles.includes('kezhang')
168
+        },
169
+        roles() {
170
+            return this.$store.state.user.roles
171
+        },
172
+        rectificationSuggestionsRequired() {
173
+            let res = this.formData.checkProjectItemList || []
174
+            if (res.length == 0) {
175
+                return false
176
+            }
177
+            return res.some(item => {
178
+                return item.children.some(child => child.scoreLevel == 'UNQUALIFIED')
179
+            })
180
+        },
181
+        // 判断是否所有检查项都符合
182
+        allItemsQualified() {
183
+            let res = this.formData.checkProjectItemList || []
184
+            if (res.length == 0) {
185
+                return false
186
+            }
187
+            return res.every(item => {
188
+                return item.children.every(child => child.scoreLevel == 'QUALIFIED')
189
+            })
190
+        },
191
+        formDisabled() {
192
+            return this.formData.checkRecordStatus != 'DRAFT' && this.type == 'document'
193
+        },
194
+        getCheckLabel() {
195
+            if (this.formData.checkedLevel == checkedLevelEnums.TEAM_LEVEL) {
196
+                return '被检查班组'
197
+            }
198
+            if (this.formData.checkedLevel == checkedLevelEnums.PERSONNEL_LEVEL) {
199
+                this.getDeptLeader()
200
+                return '被检查人'
201
+            }
202
+            if (this.formData.checkedLevel == checkedLevelEnums.DEPARTMENT_LEVEL) {
203
+                return '被检查主管'
204
+            }
205
+            if (this.formData.checkedLevel == checkedLevelEnums.BRIGADE_LEVEL) {
206
+                return '被检查大队'
207
+            }
208
+        },
209
+        // 获取验证字段名
210
+        getCheckFieldName() {
211
+            if (this.formData.checkedLevel == checkedLevelEnums.TEAM_LEVEL) {
212
+                return 'checkedTeamId'
213
+            }
214
+            if (this.formData.checkedLevel == checkedLevelEnums.PERSONNEL_LEVEL) {
215
+                return 'checkedPersonnelId'
216
+            }
217
+            if (this.formData.checkedLevel == checkedLevelEnums.DEPARTMENT_LEVEL) {
218
+                return 'checkedDepartmentId'
219
+            }
220
+            if (this.formData.checkedLevel == checkedLevelEnums.BRIGADE_LEVEL) {
221
+                return 'checkedBrigadeId'
222
+            }
223
+        },
224
+        // 验证规则
225
+        rules() {
226
+            let changeRule = {}
227
+            if (this.formData.checkedLevel == checkedLevelEnums.TEAM_LEVEL) {
228
+                changeRule = {
229
+                    checkedTeamId: {
230
+                        rules: [{ required: true, errorMessage: '请选择被检查班组' }]
231
+                    }
232
+                }
233
+            }
234
+            if (this.formData.checkedLevel == checkedLevelEnums.PERSONNEL_LEVEL) {
235
+                changeRule = {
236
+                    checkedPersonnelId: {
237
+                        rules: [{ required: true, errorMessage: '请选择被检查人' }]
238
+                    }
239
+                }
240
+            }
241
+            if (this.formData.checkedLevel == checkedLevelEnums.DEPARTMENT_LEVEL) {
242
+                changeRule = {
243
+                    checkedDepartmentId: {
244
+                        rules: [{ required: true, errorMessage: '请选择被检查主管' }]
245
+                    }
246
+                }
247
+            }
248
+            if (this.formData.checkedLevel == checkedLevelEnums.BRIGADE_LEVEL) {
249
+                changeRule = {
250
+                    checkedBrigadeId: {
251
+                        rules: [{ required: true, errorMessage: '请选择被检查大队' }]
252
+                    }
253
+                }
254
+            }
255
+            return {
256
+                ...changeRule,
257
+                checkLocation: {
258
+                    rules: [
259
+                        {
260
+                            required: true,
261
+                            errorMessage: '请选择检查地点'
262
+                        },
263
+                        {
264
+                            validateFunction: (rule, value, data, callback) => {
265
+                                console.log(value, "value")
266
+                                if (!value) {
267
+                                    // 检查是否选择了完整的检查地点(终端、区域、通道)
268
+                                    callback('请选择完整的检查地点');
269
+                                } else {
270
+                                    callback();
271
+                                }
272
+                            }
273
+                        }
274
+                    ]
275
+                },
276
+                checkTime: {
277
+                    rules: [{ required: true, errorMessage: '请选择检查时间' }]
278
+                },
279
+                responsibleUserId: {
280
+                    rules: [{ required: true, errorMessage: '请输入责任人姓名' }]
281
+                },
282
+                rectificationDeadline: {
283
+                    rules: [{ required: true, errorMessage: '请选择整改期限' }]
284
+                },
285
+                rectificationSuggestions: {
286
+                    rules: [{ required: true, errorMessage: '请输入整改要求' }]
287
+                }
288
+            }
289
+        },
290
+    },
291
+    data() {
292
+        return {
293
+            teams: [],//被检查班组选项
294
+            departments: [],//被检查主管选项
295
+            userOptions: [],//全部的人
296
+            brigades: [],
297
+            checkcheckedTeamIdOptions: [],
298
+            // 表单数据
299
+            formData: {
300
+                // 基本信息
301
+                checkerName: '',
302
+                checkerId: '',
303
+                frequency: '',
304
+                problemDescription: '',
305
+                checkedTeamId: '',
306
+                checkLocation: '',
307
+                checkTime: formatTime(new Date(), 'YYYY-MM-DD hh:mm:ss'),
308
+
309
+                // 巡检项目
310
+                baseAttachmentList: [],
311
+                // 整改要求
312
+                responsibleUserId: '',
313
+                rectificationDeadline: '',
314
+                rectificationSuggestions: ''
315
+            },
316
+
317
+            location_options: [],
318
+            // 其他数据
319
+            startDate: '2020-01-01',
320
+
321
+            imageStyles: {
322
+                width: 80,
323
+                height: 80,
324
+                border: {
325
+                    color: '#eee',
326
+                    width: '1px',
327
+                    style: 'solid'
328
+                }
329
+            },
330
+            taskId: '',
331
+            type: '',
332
+            deptTree: [],
333
+            //被检查人的对象,在检查级别为班组的时候,需要塞到checkUserList里传给后端
334
+            checkedUserObj: {}
335
+        }
336
+    },
337
+    onLoad(options) {
338
+
339
+        this.taskId = options.id; // 获取路由参数中的id
340
+        this.type = options.type; // 获取路由参数中的type
341
+
342
+        this.initPageData();
343
+    },
344
+    mounted() {
345
+
346
+
347
+    },
348
+    methods: {
349
+        getDeptLeader() {
350
+            selectDeptLeaderByUserId({
351
+                userId: this.currentUser.id,
352
+                deptType: 'DEPARTMENT'
353
+            }).then(res => {
354
+
355
+                if (res.code == 200) {
356
+                    this.formData.responsibleUserId = res.data.userId;
357
+                    this.formData.responsibleUserName = res.data.nickName;
358
+                }
359
+            })
360
+        },
361
+        // 被检查人选择
362
+        handleCheckedSelect(e) {
363
+            this.formData.checkedPersonnelId = e;
364
+            let checkedUser = this.userOptions.find(item => item.userId == e);
365
+            this.formData.checkedPersonnelName = checkedUser?.nickName;
366
+            this.checkedUserObj = checkedUser;
367
+        },
368
+        // 继续提交
369
+        handleContinueSubmit() {
370
+            // 清空表单数据
371
+            this.formData.checkedTeamId = '';
372
+            this.formData.checkLocation = [];
373
+            this.formData.checkTime = '';
374
+
375
+            // 清空巡检项目数据
376
+            if (this.formData.checkProjectItemList && this.formData.checkProjectItemList.length > 0) {
377
+                this.formData.checkProjectItemList.forEach(group => {
378
+                    if (group.children && group.children.length > 0) {
379
+                        group.children.forEach(ele => {
380
+                            // 清空检查人员列表
381
+                            ele.checkUserList = [];
382
+                            // 将评分等级设置为符合
383
+                            ele.scoreLevel = 'QUALIFIED';
384
+                        });
385
+                    }
386
+                });
387
+            }
388
+
389
+            // 清空附件列表
390
+            this.formData.baseAttachmentList = [];
391
+
392
+            // 清空整改要求数据
393
+            this.formData.responsibleUserId = '';
394
+            this.formData.rectificationDeadline = '';
395
+            //  this.formData.rectificationSuggestions = '';
396
+            this.$set(this.formData, 'rectificationSuggestions', '')
397
+
398
+            // 重置相关字段
399
+            this.formData.checkedTeamName = '';
400
+            this.formData.responsibleUserName = '';
401
+            this.formData.terminlName = '';
402
+            this.formData.terminlCode = '';
403
+            this.formData.regionalName = '';
404
+            this.formData.regionalCode = '';
405
+            this.formData.channelName = '';
406
+            this.formData.channelCode = '';
407
+
408
+            // 如果被检查主管有值,重新执行handlecheckedDepartmentIdChange
409
+            if (this.formData.checkedDepartmentId) {
410
+                this.handlecheckedDepartmentIdChange({
411
+                    detail: {
412
+                        value: [{
413
+                            text: this.formData.checkedDepartmentName,
414
+                            value: this.formData.checkedDepartmentId
415
+                        }]
416
+                    }
417
+                });
418
+            }
419
+
420
+            // 重新赋值整改期限(当前时间加4天)
421
+            const currentDate = new Date();
422
+            currentDate.setDate(currentDate.getDate() + 4);
423
+            const fourDaysLater = currentDate.toISOString().replace('T', ' ').substring(0, 19);
424
+            this.formData.rectificationDeadline = fourDaysLater;
425
+
426
+            console.log('表单已清空,可以继续提交新数据');
427
+        },
428
+        // 检查地点选择
429
+        handleCheckLocationChange(e) {
430
+            let res = e.detail.value;
431
+            this.formData.terminlName = res[0]?.text;
432
+            this.formData.terminlCode = res[0]?.value;
433
+            this.formData.regionalName = res[1]?.text;
434
+            this.formData.regionalCode = res[1]?.value;
435
+            this.formData.channelName = res[2]?.text;
436
+            this.formData.channelCode = res[2]?.value;
437
+        },
438
+        // 检查班组选择
439
+        handlecheckedTeamIdChange(e) {
440
+            const { text, value } = e.detail.value[0];
441
+            let newText = text.split('/')
442
+            this.$set(this.formData, 'checkedDeptId', value);
443
+            this.$set(this.formData, 'checkedDeptName', newText[newText.length - 1].trim());
444
+            this.$set(this.formData, 'checkedTeamName', newText[newText.length - 1]);
445
+
446
+            // 在树状结构中查找指定id的上一级id
447
+            const findParentIdInTree = (tree, targetId, parent = null) => {
448
+                for (const node of tree) {
449
+                    // 如果当前节点就是目标节点,返回父级信息
450
+                    if (node.id === targetId) {
451
+                        return parent ? {
452
+                            label: parent.label,
453
+                            id: parent.id
454
+                        } : null;
455
+                    }
456
+                    // 如果当前节点有子节点,递归查找
457
+                    if (node.children && node.children.length > 0) {
458
+                        const result = findParentIdInTree(node.children, targetId, node);
459
+                        if (result !== null) {
460
+                            return result;
461
+                        }
462
+                    }
463
+                }
464
+                return null;
465
+            };
466
+
467
+
468
+
469
+            // 直接从teams数据中查找
470
+            const { label, id } = findParentIdInTree(this.deptTree, value);
471
+
472
+
473
+            // 如果有找到上一级DEPARTMENT对象,可以在这里使用它
474
+            if (label && id) {
475
+                // 可以根据需要将部门信息设置到formData中
476
+                this.$set(this.formData, 'checkedDepartmentId', id);
477
+                this.$set(this.formData, 'checkedDepartmentName', label.trim());
478
+            } else {
479
+                console.log('未找到符合条件的上一级DEPARTMENT对象');
480
+            }
481
+
482
+            getDeptManager(value).then(res => {
483
+                console.log(res?.data?.userId, "111res")
484
+                this.$set(this.formData, 'responsibleUserId', res?.data?.userId || '');
485
+                this.$set(this.formData, 'responsibleUserName', res?.data?.nickName || '');
486
+            })
487
+        },
488
+        // 检查科选择
489
+        handlecheckedDepartmentIdChange(e) {
490
+
491
+            const { text, value } = e.detail.value[0];
492
+
493
+            let newText = text.split('/')
494
+            this.$set(this.formData, 'checkedDeptId', value);
495
+            this.$set(this.formData, 'checkedDeptName', newText[newText.length - 1].trim());
496
+            this.$set(this.formData, 'checkedDepartmentName', newText[newText.length - 1].trim());
497
+            getDeptManager(value).then(res => {
498
+                console.log(res?.data?.userId, "111res")
499
+                this.$set(this.formData, 'responsibleUserId', res?.data?.userId || '');
500
+                this.$set(this.formData, 'responsibleUserName', res?.data?.nickName || '');
501
+            })
502
+        },
503
+        handlecheckedBrigadeIdChange(e) {
504
+            const { text, value } = e.detail.value[0];
505
+            let newText = text.split('/')
506
+            this.$set(this.formData, 'checkedBrigadeName', newText[newText.length - 1].trim());
507
+            this.$set(this.formData, 'checkedDeptId', value);
508
+
509
+            this.$set(this.formData, 'checkedDeptName', newText[newText.length - 1].trim());
510
+            this.$set(this.formData, 'checkedDepartmentId', value);
511
+            this.$set(this.formData, 'checkedDepartmentName', newText[newText.length - 1].trim());
512
+            getDeptRoleUser(value, ['xingzheng']).then(res => {
513
+
514
+                this.$set(this.formData, 'responsibleUserId', res?.data[0].userId || '');
515
+                this.$set(this.formData, 'responsibleUserName', res?.data[0].nickName || '');
516
+            })
517
+        },
518
+        // 加载字典数据
519
+        async loadDictData() {
520
+            const user = await listAllUser();
521
+            this.userOptions = user.data.map(item => ({
522
+                ...item,
523
+                nickName: `${item.nickName}(${item.userName})`,
524
+            })) || [];
525
+            const deptTree = await getDeptList();
526
+            this.deptTree = deptTree.data || [];
527
+            this.teams = buildTeamOptions(deptTree.data || []);
528
+            this.departments = buildManagerOptions(deptTree.data || []);
529
+            this.brigades = buildBrigadeOptions(deptTree.data || []);
530
+            console.log(this.departments, "this.departments")
531
+            const [positionRes] = await Promise.all([
532
+                treeSelectByType("POSITION", 3),
533
+            ]);
534
+
535
+            const convertTree = (list = []) =>
536
+                list.map(node => ({
537
+                    text: node.label,
538
+                    value: node.code,
539
+                    children: node.children ? convertTree(node.children) : null
540
+                }))
541
+
542
+            this.location_options = convertTree(positionRes.data || []);
543
+            // console.log(this.location_options, this.teams)
544
+            const dict = await this.useDict('check_checked_level');
545
+            this.checkcheckedTeamIdOptions = dict.check_checked_level || [];
546
+        },
547
+        async initPageData() {
548
+            try {
549
+                uni.showLoading({ title: '加载中...', mask: true });
550
+
551
+                // 如果有任务ID,获取巡检任务详情
552
+                if (this.taskId) {
553
+                    //如果是任务就调任务的详情接口,如果是检查单就调用检查单的接口
554
+                    let api = this.type === 'task' ? getCheckTask : getInspectionListById;
555
+                    const inspectionDetail = await api(this.taskId);
556
+                    this.fillFormData(inspectionDetail);
557
+                }
558
+                await this.loadDictData();
559
+
560
+                // // 获取部门列表
561
+                // const deptTree = await getDeptList();
562
+                // this.teams = this.buildDepartmentOptions(deptTree.data || []);
563
+
564
+                // // 获取位置列表
565
+                // const positionRes = await treeSelectByType("POSITION", 3);
566
+                // this.location_options = this.convertTree(positionRes.data || []);
567
+
568
+                uni.hideLoading();
569
+            } catch (err) {
570
+                uni.hideLoading();
571
+                uni.showToast({ title: '加载失败', icon: 'none' });
572
+                console.error('初始化数据失败:', err);
573
+            }
574
+        },
575
+        // buildDepartmentOptions(tree = []) {
576
+        //     const result = [];
577
+
578
+        //     function dfs(node, path = []) {
579
+        //         const currentPath = [...path, node.label];
580
+        //         // 如果是部门或班组节点
581
+        //         if (node.deptType === 'DEPARTMENT' || node.deptType === 'TEAMS') {
582
+        //             result.push({
583
+        //                 text: currentPath.join(' / '),
584
+        //                 value: node.id
585
+        //             });
586
+        //         }
587
+        //         // 继续递归子节点
588
+        //         if (node.children && Array.isArray(node.children)) {
589
+        //             node.children.forEach(child => dfs(child, currentPath));
590
+        //         }
591
+        //     }
592
+
593
+        //     tree.forEach(root => dfs(root));
594
+        //     return result;
595
+        // },
596
+        convertTree(list = []) {
597
+            return list.map(node => ({
598
+                text: node.label,
599
+                value: node.code,
600
+                children: node.children ? this.convertTree(node.children) : null
601
+            }));
602
+        },
603
+        // 选择文件后手动上传
604
+        async onSelect(event) {
605
+            const files = await this.uploadFile(event);
606
+            console.log(files, "file", this.formData)
607
+            let fileArr = files.map(file => ({
608
+                url: file.url,
609
+                name: file.newFileName,
610
+                attachmentName: file.newFileName,
611
+                attachmentUrl: file.url,
612
+                extname: file.newFileName.split('.').pop()
613
+            }))
614
+            // 直接替换而不是追加,因为限制只能上传1张
615
+            this.formData.baseAttachmentList = [
616
+                ...this.formData.baseAttachmentList,
617
+                ...fileArr
618
+            ];
619
+
620
+        },
621
+        // 封装上传
622
+        uploadFile(event) {
623
+            return Promise.all(event.tempFilePaths.map(filePath => {
624
+                return new Promise((resolve, reject) => {
625
+                    uni.uploadFile({
626
+                        url: `${config.baseUrl}/common/upload`,
627
+                        filePath: filePath,
628
+                        name: 'file',
629
+                        header: { Authorization: 'Bearer ' + getToken() },
630
+                        formData: {
631
+                            // 可添加其他参数
632
+                        },
633
+                        success: (res) => resolve(JSON.parse(res.data)),
634
+                        fail: reject
635
+                    });
636
+                });
637
+            }));
638
+        },
639
+        // 填充表单数据
640
+        fillFormData(inspectionDetail) {
641
+            if (!inspectionDetail || !inspectionDetail.data) return;
642
+
643
+            const data = inspectionDetail.data;
644
+
645
+            // 计算当前时间加4天
646
+            const currentDate = new Date();
647
+            currentDate.setDate(currentDate.getDate() + 4);
648
+            const fourDaysLater = currentDate.toISOString().replace('T', ' ').substring(0, 19);
649
+
650
+            // 按categoryCodeOne和categoryCodeTwo分组处理checkProjectItemList
651
+            const groupedCheckProjectItemList = data.checkProjectItemList && data.checkProjectItemList.reduce((groups, item) => {
652
+                const groupKey = `${item.categoryCodeOne}_${item.categoryCodeTwo}`;
653
+                if (!groups[groupKey]) {
654
+                    groups[groupKey] = {
655
+                        categoryCodeOne: item.categoryCodeOne,
656
+                        categoryCodeTwo: item.categoryCodeTwo,
657
+                        categoryNameOne: item.categoryNameOne,
658
+                        categoryNameTwo: item.categoryNameTwo,
659
+                        children: []
660
+                    };
661
+                }
662
+                groups[groupKey].children.push({
663
+                    ...item,
664
+                    scoreLevel: item.scoreLevel || 'QUALIFIED'
665
+                });
666
+                return groups;
667
+            }, {});
668
+
669
+            // 将分组对象转换为数组
670
+            const checkProjectItemListArray = groupedCheckProjectItemList ? Object.values(groupedCheckProjectItemList) : [];
671
+
672
+            // 填充基本信息
673
+            this.formData = {
674
+                ...data,
675
+                checkTime: data.checkTime || formatTime(new Date(), 'YYYY-MM-DD hh:mm:ss'),
676
+                checkerName: this.currentUser.userInfo.nickName || this.currentUser.userInfo.userName || '',
677
+                checkerId: this.currentUser.userInfo.userId || '',
678
+                rectificationDeadline: data.rectificationDeadline || fourDaysLater,
679
+                baseAttachmentList: (data.baseAttachmentList && data.baseAttachmentList.map(item => ({
680
+                    ...item,
681
+                    url: item.attachmentUrl,
682
+                    name: item.attachmentName,
683
+                    extname: item.extname
684
+                }))) || [],
685
+                checkedTeamId: Number(data.checkedTeamId),
686
+                checkLocation: [{ text: data.terminlName, value: data.terminlCode }, { text: data.regionalName, value: data.regionalCode }, { text: data.channelName, value: data.channelCode }],
687
+                checkProjectItemList: checkProjectItemListArray
688
+            }
689
+
690
+
691
+
692
+            console.log(this.formData, "this.formData回显")
693
+        },
694
+
695
+        // 处理添加不合格人员
696
+        handleAddPersonnel(index) {
697
+            console.log('添加不合格人员,项目索引:', index);
698
+
699
+            this.$nextTick(() => {
700
+                this.$refs.xunjianCollapseItem.updateCollapseHeight();
701
+            })
702
+            // 这里可以打开人员选择弹窗或跳转到人员选择页面
703
+            uni.showToast({ title: '打开人员选择', icon: 'none' });
704
+        },
705
+
706
+        // 处理人员数据变更
707
+        handlePersonnelChange(changeData) {
708
+            const { parentIndex, childIndex, personnelData, problemDescription } = changeData;
709
+            console.log(personnelData, "personnelData")
710
+            if (parentIndex >= 0 && childIndex >= 0 &&
711
+                this.formData.checkProjectItemList &&
712
+                this.formData.checkProjectItemList[parentIndex] &&
713
+                this.formData.checkProjectItemList[parentIndex].children &&
714
+                this.formData.checkProjectItemList[parentIndex].children[childIndex]) {
715
+
716
+                // 更新对应子项目的检查人员列表和任务简介
717
+                this.formData.checkProjectItemList[parentIndex].children[childIndex].checkUserList = personnelData;
718
+                this.formData.checkProjectItemList[parentIndex].children[childIndex].problemDescription = problemDescription;
719
+                console.log('更新不合格人员数据:', parentIndex, childIndex, personnelData, problemDescription);
720
+            }
721
+        },
722
+
723
+        // 处理用户选择
724
+        handleUserSelect(value) {
725
+
726
+            const selectedUser = this.userOptions.find(user => user.userId === value);
727
+
728
+            if (selectedUser) {
729
+                this.formData.responsibleUserId = selectedUser.userId;
730
+                this.formData.responsibleUserName = selectedUser.nickName;
731
+            }
732
+        },
733
+        formateData() {
734
+            let checkedUserObj = {}
735
+            if (JSON.stringify(this.checkedUserObj) !== '{}') {
736
+                checkedUserObj = {
737
+                    ...this.checkedUserObj,
738
+                    userName: this.checkedUserObj.nickName.split('(')[0] || '',
739
+                }
740
+            }
741
+
742
+
743
+            const originalCheckProjectItemList = this.formData.checkProjectItemList
744
+                ? this.formData.checkProjectItemList.flatMap(group =>
745
+                    group.children.map(child => ({
746
+                        ...child,
747
+                        categoryCodeOne: group.categoryCodeOne,
748
+                        categoryCodeTwo: group.categoryCodeTwo,
749
+                        categoryNameOne: group.categoryNameOne,
750
+                        categoryNameTwo: group.categoryNameTwo,
751
+                        //如果是班组,自动赋值被检查人给checkUserList
752
+                        ...(this.formData.checkLevel == 'TEAM_LEVEL' && child.scoreLevel == "UNQUALIFIED" ? { checkUserList: [checkedUserObj] } : {}),
753
+                    }))
754
+                )
755
+                : [];
756
+
757
+            let payload = {
758
+                ...this.formData,
759
+                checkedTeamName: (this.formData.checkedTeamName || '').trim(),
760
+                checkProjectItemList: originalCheckProjectItemList
761
+            };
762
+
763
+            delete payload.id;
764
+            console.log(payload, "payload")
765
+            // debugger
766
+            return payload;
767
+        },
768
+        // 保存草稿
769
+        saveDraft() {
770
+            // this.$refs.form.validate().then(res => {
771
+            uni.showLoading({ title: '保存中...', mask: true });
772
+
773
+
774
+            const payload = this.formateData();
775
+
776
+
777
+            addDraftInspection({ ...payload, ...(this.formData.checkRecordStatus == 'DRAFT' ? { id: this.taskId } : {}) })
778
+                .then(() => {
779
+                    uni.showToast({ title: '草稿保存成功', icon: 'success' });
780
+                    console.log('草稿保存成功:',);
781
+                    setTimeout(() => uni.navigateBack(), 1500);
782
+                })
783
+                .catch((error) => {
784
+                    uni.showToast({ title: '草稿保存失败,请稍后重试!', icon: 'none' });
785
+                    console.error('草稿保存失败:', error);
786
+                });
787
+
788
+            // }).catch(err => {
789
+            //     console.log('表单验证失败:', err);
790
+            //     uni.showToast({ title: '请填写完整表单信息', icon: 'none' });
791
+            // });
792
+        },
793
+
794
+        // 提交表单
795
+        submitForm() {
796
+            this.$refs.form.validate().then(res => {
797
+                debugger
798
+                // 检查所有不合格项是否都填写了相关人员
799
+                let hasError = false;
800
+                let errorMessage = '';
801
+                //被检查级别科 班组
802
+                if (this.formData.checkProjectItemList && this.formData.checkProjectItemList.length > 0 && this.getCheckLabel != '被检查人') {
803
+                    for (let groupIndex = 0; groupIndex < this.formData.checkProjectItemList.length; groupIndex++) {
804
+                        const group = this.formData.checkProjectItemList[groupIndex];
805
+                        if (group.children && group.children.length > 0) {
806
+                            for (let itemIndex = 0; itemIndex < group.children.length; itemIndex++) {
807
+                                const item = group.children[itemIndex];
808
+                                if (item.scoreLevel === 'UNQUALIFIED' &&
809
+                                    ((!item.checkUserList || item.checkUserList.length === 0) && (!item.problemDescription || item.problemDescription.trim() === ''))) {
810
+                                    hasError = true;
811
+                                    errorMessage = `第${groupIndex + 1}组项目 "${item.projectName}" 标记为不符合,请选择相关人员或填写问题描述`;
812
+                                    break;
813
+                                }
814
+                            }
815
+                            if (hasError) break;
816
+                        }
817
+                    }
818
+                }
819
+
820
+                // 如果有错误,弹出提示并阻止提交
821
+                if (hasError) {
822
+                    uni.showToast({ title: errorMessage, icon: 'none' });
823
+                    return;
824
+                }
825
+
826
+                // 验证通过,继续提交
827
+                uni.showLoading({ title: '提交中...', mask: true });
828
+
829
+                const payload = this.formateData();
830
+                debugger
831
+
832
+                submitInspection({ ...payload, ...(this.type == 'task' ? {} : { id: this.taskId }) })
833
+                    .then(() => {
834
+                        uni.showToast({ title: '提交成功', icon: 'success' });
835
+                        this.$refs.submitResult.open();
836
+                    })
837
+                    .catch((error) => {
838
+                        uni.showToast({ title: '操作失败,请稍后重试!', icon: 'none' });
839
+                        console.error('提交失败:', error);
840
+                    });
841
+
842
+            }).catch(err => {
843
+                console.log('表单验证失败:', err);
844
+                uni.showToast({ title: '请填写完整表单信息', icon: 'none' });
845
+            });
846
+        }
847
+
848
+    }
849
+}
850
+</script>
851
+
852
+<style lang="scss" scoped>
853
+.report-container {
854
+    min-height: 100vh;
855
+    padding-top: 35px;
856
+
857
+    .card {
858
+        border-radius: 8px;
859
+        margin: 15px 0;
860
+        overflow: hidden;
861
+        box-shadow: rgba(0, 0, 0, 0.08) 0px 0px 3px 1px;
862
+    }
863
+}
864
+
865
+/* 折叠面板标题 */
866
+.collapse-title {
867
+    display: flex;
868
+    flex-direction: column;
869
+    padding: 12px 15px;
870
+
871
+    .collapse-summary {
872
+        font-size: 12px;
873
+        color: #999;
874
+        margin-top: 4px;
875
+    }
876
+}
877
+
878
+/* 表单项样式 */
879
+::v-deep .uni-forms-item {
880
+    margin-bottom: 0;
881
+    padding: 0 15px;
882
+
883
+    .uni-forms-item__label {
884
+        padding: 12px 0 8px;
885
+        font-size: 14px;
886
+        color: #666;
887
+        width: auto !important;
888
+    }
889
+
890
+    .uni-forms-item__content {
891
+        padding: 0 0 12px;
892
+        border-bottom: 1px solid #f0f0f0;
893
+    }
894
+
895
+    &:last-child .uni-forms-item__content {
896
+        border-bottom: none;
897
+    }
898
+}
899
+
900
+.submit-btn {
901
+    margin-top: 20px;
902
+    width: calc(100% - 30px);
903
+    margin-left: 15px;
904
+}
905
+
906
+/* 底部弹窗样式调整 */
907
+::v-deep .uni-popup__wrapper {
908
+    border-radius: 16px 16px 0 0;
909
+
910
+    .uni-popup__wrapper-box {
911
+        max-height: 70vh;
912
+        overflow-y: auto;
913
+    }
914
+
915
+    .uni-data-pickerview {
916
+        padding-bottom: 20px;
917
+    }
918
+}
919
+
920
+:v-deep .uni-input-input:disabled {
921
+    background-color: #f5f5f5;
922
+}
923
+
924
+/* 巡检项目卡片样式 */
925
+.item-title {
926
+    display: flex;
927
+    justify-content: space-between;
928
+    align-items: center;
929
+    width: 100%;
930
+    font-size: 16px;
931
+    font-weight: bold;
932
+    color: #333;
933
+}
934
+
935
+.item-content {
936
+    margin-top: 12px;
937
+
938
+    .item-description {
939
+        display: flex;
940
+        flex-direction: column;
941
+
942
+        .description-label {
943
+            font-size: 14px;
944
+            color: #666;
945
+            margin-bottom: 4px;
946
+        }
947
+
948
+        .description-text {
949
+            font-size: 14px;
950
+            color: #333;
951
+            line-height: 1.5;
952
+        }
953
+    }
954
+}
955
+
956
+.upload-section {
957
+    margin-top: 20px;
958
+    padding-top: 15px;
959
+    border-top: 1px solid #f0f0f0;
960
+
961
+    .upload-title {
962
+        display: block;
963
+        font-size: 16px;
964
+        font-weight: bold;
965
+        color: #333;
966
+        margin-bottom: 12px;
967
+    }
968
+}
969
+
970
+/* 按钮组样式 - 左右布局 */
971
+.button-group {
972
+    display: flex;
973
+    justify-content: space-between;
974
+    align-items: center;
975
+    padding: 0 15px;
976
+    margin-top: 20px;
977
+    gap: 15px;
978
+}
979
+
980
+.button-group .custom-btn-white,
981
+.button-group .custom-btn-normal {
982
+    flex: 1;
983
+    margin-top: 0;
984
+    width: auto;
985
+}
986
+</style>

+ 43 - 0
src/pages/common/textview/index.vue

@@ -0,0 +1,43 @@
1
+<template>
2
+  <view>
3
+    <uni-card class="view-title" :title="title">
4
+      <text class="uni-body view-content">{{ content }}</text>
5
+    </uni-card>
6
+  </view>
7
+</template>
8
+
9
+<script>
10
+  export default {
11
+    data() {
12
+      return {
13
+        title: '',
14
+        content: ''
15
+      }
16
+    },
17
+    onLoad(options) {
18
+      this.title = options.title
19
+      this.content = options.content
20
+      uni.setNavigationBarTitle({
21
+        title: options.title
22
+      })
23
+    }
24
+  }
25
+</script>
26
+
27
+<style scoped>
28
+  page {
29
+    background-color: #ffffff;
30
+  }
31
+
32
+  .view-title {
33
+    font-weight: bold;
34
+  }
35
+
36
+  .view-content {
37
+    font-size: 26rpx;
38
+    padding: 12px 5px 0;
39
+    color: #333;
40
+    line-height: 24px;
41
+    font-weight: normal;
42
+  }
43
+</style>

+ 34 - 0
src/pages/common/webview/index.vue

@@ -0,0 +1,34 @@
1
+<template>
2
+  <view v-if="params.url">
3
+    <web-view :webview-styles="webviewStyles" :src="`${params.url}`"></web-view>
4
+  </view>
5
+</template>
6
+
7
+<script>
8
+  export default {
9
+    data() {
10
+      return {
11
+        params: {},
12
+        webviewStyles: {
13
+          progress: {
14
+            color: "#FF3333"
15
+          }
16
+        }
17
+      }
18
+    },
19
+    props: {
20
+      src: {
21
+        type: [String],
22
+        default: null
23
+      }
24
+    },
25
+    onLoad(event) {
26
+      this.params = event
27
+      if (event.title) {
28
+        uni.setNavigationBarTitle({
29
+          title: event.title
30
+        })
31
+      }
32
+    }
33
+  }
34
+</script>

+ 0 - 0
src/pages/daily-exam/answer/index.vue


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels