Преглед изворни кода

Merge branch 'master' of http://git.sundot.cn/chongqing/chongqing-app

huoyi пре 5 дана
родитељ
комит
9219cb82bc
38 измењених фајлова са 2704 додато и 546 уклоњено
  1. 6 5
      src/components/SectionTitle.vue
  2. 32 8
      src/pages.json
  3. 809 0
      src/pages/ai-chat/index.vue
  4. 14 5
      src/pages/components/AreaDistribution.vue
  5. 2 2
      src/pages/components/AttendanceStatus.vue
  6. 35 16
      src/pages/components/DailySeizureChart.vue
  7. 6 6
      src/pages/components/DeptSelector.vue
  8. 8 8
      src/pages/components/DeptStats.vue
  9. 2 2
      src/pages/components/DutyInfo.vue
  10. 125 0
      src/pages/components/EmployeeTreeNode.vue
  11. 7 7
      src/pages/components/GroupMemberTable.vue
  12. 11 8
      src/pages/components/InterceptionDistribution.vue
  13. 20 10
      src/pages/components/ItemDistribution.vue
  14. 45 17
      src/pages/components/MemberBasicDistribution.vue
  15. 66 30
      src/pages/components/MemberPositionDistribution.vue
  16. 9 11
      src/pages/components/PassengerChart.vue
  17. 109 56
      src/pages/components/ProfileRadar.vue
  18. 36 6
      src/pages/components/SecurityTestCharts.vue
  19. 11 8
      src/pages/components/SeizedNumAll.vue
  20. 19 11
      src/pages/components/SeizureInfo.vue
  21. 12 9
      src/pages/components/SupervisionDistribution.vue
  22. 7 7
      src/pages/components/TeamMemberTable.vue
  23. 17 8
      src/pages/components/UnsafeItemsChart.vue
  24. 18 9
      src/pages/components/UnsafePositionChart.vue
  25. 17 8
      src/pages/components/UnsafeTypesChart.vue
  26. 28 30
      src/pages/deptProfile/index.vue
  27. 546 172
      src/pages/employeeProfile/index.vue
  28. 27 29
      src/pages/groupProfile/index.vue
  29. 3 0
      src/pages/home/index.vue
  30. 2 2
      src/pages/login.vue
  31. 499 0
      src/pages/organizationStruct/index.vue
  32. 0 0
      src/pages/organizationStruct/组织架构
  33. 105 0
      src/pages/profileManage/index.vue
  34. 20 27
      src/pages/stationProfile/index.vue
  35. 24 27
      src/pages/teamProfile/index.vue
  36. 7 2
      src/pages/work/index.vue
  37. BIN
      src/static/images/tabbar/ai.png
  38. BIN
      src/static/images/tabbar/ai_.png

+ 6 - 5
src/components/SectionTitle.vue

@@ -26,11 +26,12 @@ export default {
26 26
 
27 27
 <style lang="scss" scoped>
28 28
 .section-wrapper {
29
-    background: rgba(45, 42, 85, 0.6);
29
+    background: #fff;
30 30
     border-radius: 20rpx;
31 31
     padding: 32rpx;
32 32
     margin-bottom: 24rpx;
33
-    border: 1rpx solid rgba(167, 139, 250, 0.2);
33
+    border: 1rpx solid #e0e0e0;
34
+    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
34 35
 }
35 36
 
36 37
 .section-header {
@@ -43,7 +44,7 @@ export default {
43 44
 .section-title {
44 45
     font-size: 30rpx;
45 46
     font-weight: 600;
46
-    color: #A78BFA;
47
+    color: #333;
47 48
     position: relative;
48 49
     padding-left: 16rpx;
49 50
 
@@ -55,13 +56,13 @@ export default {
55 56
         transform: translateY(-50%);
56 57
         width: 4rpx;
57 58
         height: 28rpx;
58
-        background: #A78BFA;
59
+        background: #60A5FA;
59 60
         border-radius: 2rpx;
60 61
     }
61 62
 }
62 63
 
63 64
 .section-header-right {
64 65
     font-size: 24rpx;
65
-    color: rgba(255, 255, 255, 0.5);
66
+    color: #666;
66 67
 }
67 68
 </style>

+ 32 - 8
src/pages.json

@@ -1,6 +1,13 @@
1 1
 {
2 2
   "pages": [
3 3
     {
4
+      "path": "pages/ai-chat/index",
5
+      "style": {
6
+        "navigationBarTitleText": "AI数据助手",
7
+        "navigationStyle": "custom"
8
+      }
9
+    },
10
+    {
4 11
       "path": "pages/login",
5 12
       "style": {
6 13
         "navigationBarTitleText": "登录"
@@ -117,6 +124,12 @@
117 124
       }
118 125
     },
119 126
     {
127
+      "path": "pages/profileManage/index",
128
+      "style": {
129
+        "navigationBarTitleText": "画像管理"
130
+      }
131
+    },
132
+    {
120 133
       "path": "pages/stationProfile/index",
121 134
       "style": {
122 135
         "navigationBarTitleText": "站画像"
@@ -134,7 +147,7 @@
134 147
         "navigationBarTitleText": "班组画像"
135 148
       }
136 149
     },
137
-        {
150
+    {
138 151
       "path": "pages/groupProfile/index",
139 152
       "style": {
140 153
         "navigationBarTitleText": "小组画像"
@@ -315,6 +328,12 @@
315 328
       }
316 329
     },
317 330
     {
331
+      "path": "pages/organizationStruct/index",
332
+      "style": {
333
+        "navigationBarTitleText": "组织架构"
334
+      }
335
+    },
336
+    {
318 337
       "path": "pages/inspectionChecklist/index",
319 338
       "style": {
320 339
         "navigationBarTitleText": "巡视检查列表"
@@ -373,18 +392,23 @@
373 392
       //   "text": "首页"
374 393
       // },
375 394
       {
376
-        "pagePath": "pages/work/index",
377
-        "iconPath": "static/images/tabbar/work.png",
378
-        "selectedIconPath": "static/images/tabbar/work_.png",
379
-        "text": "工作台"
380
-      },
381
-      {
382 395
         "pagePath": "pages/myToDoList/index",
383 396
         "iconPath": "/static/images/tabbar/message.png",
384 397
         "selectedIconPath": "/static/images/tabbar/message_.png",
385 398
         "text": "消息"
386 399
       },
387
-      
400
+      {
401
+        "pagePath": "pages/ai-chat/index",
402
+        "iconPath": "static/images/tabbar/ai.png",
403
+        "selectedIconPath": "static/images/tabbar/ai_.png",
404
+        "text": "AI问答"
405
+      },
406
+      {
407
+        "pagePath": "pages/work/index",
408
+        "iconPath": "static/images/tabbar/work.png",
409
+        "selectedIconPath": "static/images/tabbar/work_.png",
410
+        "text": "工作台"
411
+      },
388 412
       {
389 413
         "pagePath": "pages/mine/index",
390 414
         "iconPath": "static/images/tabbar/mine.png",

+ 809 - 0
src/pages/ai-chat/index.vue

@@ -0,0 +1,809 @@
1
+<template>
2
+  <view class="ai-chat-page">
3
+    <!-- 顶部导航 -->
4
+    <view class="nav-bar">
5
+      <view class="back-btn" @click="goBack">
6
+        <text class="back-icon">‹</text>
7
+      </view>
8
+      <text class="nav-title">AI数据助手</text>
9
+      <view class="nav-right" @click="clearChat">
10
+        <text class="clear-btn">清空</text>
11
+      </view>
12
+    </view>
13
+
14
+    <!-- 消息列表 -->
15
+    <scroll-view
16
+      class="msg-list"
17
+      scroll-y
18
+      :scroll-top="scrollTop"
19
+      :scroll-with-animation="true"
20
+      @scrolltolower="onScrollBottom"
21
+    >
22
+      <!-- 欢迎语 -->
23
+      <view v-if="messages.length === 0" class="welcome">
24
+        <image class="welcome-icon" src="/static/images/logo.png" mode="aspectFit" />
25
+        <text class="welcome-title">您好!我是机场安检数据助手</text>
26
+        <text class="welcome-sub">请问有什么可以帮您查询的?</text>
27
+        <view class="example-tags">
28
+          <view
29
+            v-for="tag in exampleTags"
30
+            :key="tag"
31
+            class="tag"
32
+            @click="sendExample(tag)"
33
+          >{{ tag }}</view>
34
+        </view>
35
+      </view>
36
+
37
+      <!-- 消息气泡 -->
38
+      <view
39
+        v-for="(msg, idx) in messages"
40
+        :key="idx"
41
+        :class="['msg-row', msg.role === 'user' ? 'msg-user' : 'msg-assistant']"
42
+      >
43
+        <!-- AI头像 -->
44
+        <view v-if="msg.role === 'assistant'" class="avatar">
45
+          <text class="avatar-text">AI</text>
46
+        </view>
47
+
48
+        <view class="bubble-wrap">
49
+          <!-- 进度提示(流式过程中) -->
50
+          <view v-if="msg.steps && msg.steps.length" class="steps">
51
+            <view v-for="(step, si) in msg.steps" :key="si" :class="['step', step.status]">
52
+              <text class="step-dot">{{ step.status === 'done' ? '✓' : step.status === 'running' ? '…' : '·' }}</text>
53
+              <text class="step-name">{{ step.name }}</text>
54
+              <text v-if="step.duration" class="step-time"> {{ step.duration }}s</text>
55
+            </view>
56
+          </view>
57
+
58
+          <!-- 文字回答(知识库/AI直接回答) -->
59
+          <view v-if="msg.answer" class="bubble knowledge">
60
+            <text class="bubble-text" selectable>{{ msg.answer }}</text>
61
+            <view v-if="msg.sources && msg.sources.length" class="sources">
62
+              <text class="source-label">来源:</text>
63
+              <text class="source-item" v-for="s in msg.sources" :key="s.filename">{{ s.filename }}</text>
64
+            </view>
65
+          </view>
66
+
67
+          <!-- 数据查询结果 -->
68
+          <view v-else-if="msg.role === 'assistant' && !msg.loading" class="bubble data">
69
+            <text v-if="msg.message" class="result-msg" :class="{ error: msg.isError }">{{ msg.message }}</text>
70
+
71
+            <!-- 数据表格 -->
72
+            <view v-if="msg.rows && msg.rows.length" class="data-table">
73
+              <scroll-view scroll-x class="table-scroll">
74
+                <view class="table-head">
75
+                  <text v-for="col in msg.columns" :key="col" class="th">{{ col }}</text>
76
+                </view>
77
+                <view v-for="(row, ri) in msg.rows" :key="ri" :class="['table-row', ri % 2 === 0 ? 'even' : '']">
78
+                  <text v-for="col in msg.columns" :key="col" class="td">{{ row[col] != null ? row[col] : '-' }}</text>
79
+                </view>
80
+              </scroll-view>
81
+            </view>
82
+
83
+            <!-- SQL 折叠 -->
84
+            <view v-if="msg.sql" class="sql-block">
85
+              <view class="sql-toggle" @click="toggleSql(idx)">
86
+                <text class="sql-label">SQL</text>
87
+                <text class="sql-arrow">{{ msg.showSql ? '▲' : '▼' }}</text>
88
+              </view>
89
+              <view v-if="msg.showSql" class="sql-code">
90
+                <text selectable>{{ msg.sql }}</text>
91
+              </view>
92
+            </view>
93
+          </view>
94
+
95
+          <!-- 用户消息气泡 -->
96
+          <view v-if="msg.role === 'user'" class="bubble user">
97
+            <text class="bubble-text" selectable>{{ msg.text }}</text>
98
+          </view>
99
+
100
+          <!-- 加载中 -->
101
+          <view v-if="msg.loading" class="bubble loading">
102
+            <view class="dot-loader">
103
+              <view class="dot" /><view class="dot" /><view class="dot" />
104
+            </view>
105
+          </view>
106
+        </view>
107
+      </view>
108
+
109
+      <!-- 底部留白 -->
110
+      <view style="height: 20rpx;" />
111
+    </scroll-view>
112
+
113
+    <!-- 意图澄清 -->
114
+    <view v-if="clarifyQuestion" class="clarify-bar">
115
+      <text class="clarify-text">{{ clarifyQuestion }}</text>
116
+    </view>
117
+
118
+    <!-- 输入区 -->
119
+    <view class="input-bar">
120
+      <!-- 麦克风按钮(同时支持触摸和鼠标,方便桌面浏览器测试) -->
121
+      <view
122
+        :class="['mic-btn', recording ? 'recording' : '']"
123
+        @touchstart.prevent="onPressStart"
124
+        @touchend.prevent="onPressEnd"
125
+        @touchcancel.prevent="onPressCancel"
126
+        @mousedown.prevent="onMouseDown"
127
+      >
128
+        <text class="mic-icon">{{ recording ? '●' : '🎤' }}</text>
129
+      </view>
130
+
131
+      <input
132
+        v-model="inputText"
133
+        class="input"
134
+        :placeholder="recording ? '正在录音...' : '请输入您的问题...'"
135
+        :disabled="loading || recording"
136
+        confirm-type="send"
137
+        @confirm="sendMessage"
138
+      />
139
+      <view :class="['send-btn', (loading || recording) ? 'disabled' : '']" @click="sendMessage">
140
+        <text class="send-text">{{ loading ? '…' : '发送' }}</text>
141
+      </view>
142
+    </view>
143
+
144
+    <!-- 录音提示浮层 -->
145
+    <view v-if="recording" class="recording-overlay">
146
+      <view class="recording-inner">
147
+        <text class="recording-icon">🎤</text>
148
+        <text class="recording-tip">松手发送,上滑取消</text>
149
+        <view class="recording-waves">
150
+          <view v-for="i in 5" :key="i" class="wave" :style="{ animationDelay: (i * 0.1) + 's' }" />
151
+        </view>
152
+      </view>
153
+    </view>
154
+  </view>
155
+</template>
156
+
157
+<script>
158
+const AI_BASE = 'http://airport-test.samsundot.com:9015'
159
+const AI_URL  = AI_BASE + '/api/query-stream'
160
+const ASR_URL = AI_BASE + '/api/asr'
161
+
162
+export default {
163
+  name: 'AiChat',
164
+  data() {
165
+    return {
166
+      inputText: '',
167
+      messages: [],
168
+      loading: false,
169
+      scrollTop: 0,
170
+      clarifyQuestion: '',
171
+      sessionId: 'app_' + Date.now(),
172
+      history: [],
173
+      exampleTags: ['各班组查获排名', '打火机查获数量', '旅检一部人员资质', '全站党员人数'],
174
+      // 录音相关(Web Audio 直接采集 PCM)
175
+      recording: false,
176
+      pressing: false,
177
+      recordCancelled: false,
178
+      audioCtx: null,
179
+      mediaStream: null,
180
+      sourceNode: null,
181
+      scriptNode: null,
182
+      pcmChunks: [],
183
+      pcmSampleRate: 16000,
184
+    }
185
+  },
186
+  methods: {
187
+    // ============ 按压事件包装 ============
188
+    onMouseDown(e) {
189
+      // 鼠标:松手可能在按钮外,用 window 级监听
190
+      this._winUp = () => this.onPressEnd()
191
+      window.addEventListener('mouseup', this._winUp, { once: true })
192
+      this.onPressStart(e)
193
+    },
194
+    onPressStart(e) {
195
+      if (this.loading || this.pressing) return
196
+      this.pressing = true
197
+      this.startRecord()
198
+    },
199
+    onPressEnd(e) {
200
+      if (!this.pressing) return
201
+      this.pressing = false
202
+      this.stopRecord()
203
+    },
204
+    onPressCancel(e) {
205
+      if (!this.pressing) return
206
+      this.pressing = false
207
+      this.recordCancelled = true
208
+      this.stopRecord()
209
+    },
210
+
211
+    // ============ 录音相关(Web Audio 直接采集原始 PCM)============
212
+    async startRecord() {
213
+      this.recordCancelled = false
214
+      this.pcmChunks = []
215
+
216
+      try {
217
+        const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
218
+        // 异步竞态:若 await 期间已松手,直接放弃
219
+        if (!this.pressing) { stream.getTracks().forEach(t => t.stop()); return }
220
+        this.mediaStream = stream
221
+
222
+        const AudioCtx = window.AudioContext || window.webkitAudioContext
223
+        const ctx = new AudioCtx()
224
+        if (ctx.state === 'suspended') await ctx.resume()
225
+        this.audioCtx = ctx
226
+        this.pcmSampleRate = ctx.sampleRate
227
+
228
+        const source = ctx.createMediaStreamSource(stream)
229
+        const node = ctx.createScriptProcessor(4096, 1, 1)
230
+        node.onaudioprocess = (ev) => {
231
+          // 只要节点还连着就采集(不依赖 recording 标志,避免漏帧)
232
+          this.pcmChunks.push(new Float32Array(ev.inputBuffer.getChannelData(0)))
233
+        }
234
+        source.connect(node)
235
+        node.connect(ctx.destination)
236
+
237
+        this.sourceNode = source
238
+        this.scriptNode = node
239
+        this.recording = true
240
+
241
+        // 若设置过程中已松手,立即停止并提交
242
+        if (!this.pressing) this.stopRecord()
243
+      } catch (err) {
244
+        this.recording = false
245
+        let msg = '无法访问麦克风'
246
+        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
247
+          msg = '当前环境不支持录音(需HTTPS或localhost)'
248
+        } else if (err && err.name === 'NotAllowedError') {
249
+          msg = '麦克风权限被拒绝,请在浏览器允许'
250
+        } else if (err && err.name === 'NotFoundError') {
251
+          msg = '未检测到麦克风设备'
252
+        }
253
+        uni.showToast({ title: msg, icon: 'none', duration: 2500 })
254
+      }
255
+    },
256
+
257
+    _teardownAudio() {
258
+      try { if (this.scriptNode) { this.scriptNode.disconnect(); this.scriptNode.onaudioprocess = null } } catch (e) {}
259
+      try { if (this.sourceNode) this.sourceNode.disconnect() } catch (e) {}
260
+      try { if (this.mediaStream) this.mediaStream.getTracks().forEach(t => t.stop()) } catch (e) {}
261
+      try { if (this.audioCtx) this.audioCtx.close() } catch (e) {}
262
+      this.scriptNode = null
263
+      this.sourceNode = null
264
+      this.mediaStream = null
265
+      this.audioCtx = null
266
+    },
267
+
268
+    stopRecord() {
269
+      if (!this.recording) return
270
+      this.recording = false
271
+      const rate = this.pcmSampleRate
272
+      const chunks = this.pcmChunks
273
+      this._teardownAudio()
274
+      if (!this.recordCancelled) this.submitAudio(chunks, rate)
275
+    },
276
+
277
+    async submitAudio(chunks, srcRate) {
278
+      // 合并所有 PCM 帧
279
+      let total = 0
280
+      for (const c of chunks) total += c.length
281
+      if (total < srcRate * 0.3) {  // 少于0.3秒视为无效
282
+        uni.showToast({ title: '录音太短,请长按说话', icon: 'none' })
283
+        return
284
+      }
285
+      const merged = new Float32Array(total)
286
+      let pos = 0
287
+      for (const c of chunks) { merged.set(c, pos); pos += c.length }
288
+
289
+      uni.showLoading({ title: '识别中...' })
290
+      try {
291
+        const wavBlob = this.encodeWav16k(merged, srcRate)
292
+        const formData = new FormData()
293
+        formData.append('file', wavBlob, 'voice.wav')
294
+
295
+        const resp = await fetch(ASR_URL, { method: 'POST', body: formData })
296
+        const data = await resp.json()
297
+        uni.hideLoading()
298
+        if (data.success && data.text) {
299
+          this.inputText = data.text
300
+          this.$nextTick(() => this.sendMessage())
301
+        } else {
302
+          uni.showToast({ title: data.message || '未识别到内容', icon: 'none' })
303
+        }
304
+      } catch (err) {
305
+        uni.hideLoading()
306
+        uni.showToast({ title: '识别失败:' + (err.message || '请重试'), icon: 'none' })
307
+      }
308
+    },
309
+
310
+    // Float32 PCM → 混(已单声道)→ 重采样16k → 16bit PCM WAV
311
+    encodeWav16k(mono, srcRate) {
312
+      const dstRate = 16000
313
+      const dstLen = Math.round(mono.length * dstRate / srcRate)
314
+      const out = new Float32Array(dstLen)
315
+      const ratio = srcRate / dstRate
316
+      for (let i = 0; i < dstLen; i++) {
317
+        const p = i * ratio
318
+        const idx = Math.floor(p)
319
+        const frac = p - idx
320
+        const s0 = mono[idx] || 0
321
+        const s1 = mono[idx + 1] !== undefined ? mono[idx + 1] : s0
322
+        out[i] = s0 + (s1 - s0) * frac
323
+      }
324
+
325
+      const buffer = new ArrayBuffer(44 + dstLen * 2)
326
+      const view = new DataView(buffer)
327
+      const writeStr = (off, str) => { for (let i = 0; i < str.length; i++) view.setUint8(off + i, str.charCodeAt(i)) }
328
+      writeStr(0, 'RIFF')
329
+      view.setUint32(4, 36 + dstLen * 2, true)
330
+      writeStr(8, 'WAVE')
331
+      writeStr(12, 'fmt ')
332
+      view.setUint32(16, 16, true)
333
+      view.setUint16(20, 1, true)
334
+      view.setUint16(22, 1, true)
335
+      view.setUint32(24, dstRate, true)
336
+      view.setUint32(28, dstRate * 2, true)
337
+      view.setUint16(32, 2, true)
338
+      view.setUint16(34, 16, true)
339
+      writeStr(36, 'data')
340
+      view.setUint32(40, dstLen * 2, true)
341
+      let off = 44
342
+      for (let i = 0; i < dstLen; i++) {
343
+        const s = Math.max(-1, Math.min(1, out[i]))
344
+        view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true)
345
+        off += 2
346
+      }
347
+      return new Blob([view], { type: 'audio/wav' })
348
+    },
349
+
350
+    // ============ 导航 ============
351
+    goBack() {
352
+      uni.navigateBack()
353
+    },
354
+
355
+    clearChat() {
356
+      this.messages = []
357
+      this.history = []
358
+      this.clarifyQuestion = ''
359
+      this.sessionId = 'app_' + Date.now()
360
+    },
361
+
362
+    sendExample(text) {
363
+      this.inputText = text
364
+      this.sendMessage()
365
+    },
366
+
367
+    toggleSql(idx) {
368
+      this.$set(this.messages[idx], 'showSql', !this.messages[idx].showSql)
369
+    },
370
+
371
+    onScrollBottom() {},
372
+
373
+    scrollToBottom() {
374
+      this.$nextTick(() => {
375
+        this.scrollTop = 999999
376
+      })
377
+    },
378
+
379
+    getUserId() {
380
+      try {
381
+        const userStr = uni.getStorageSync('userInfo') || uni.getStorageSync('user')
382
+        if (userStr) {
383
+          const u = typeof userStr === 'string' ? JSON.parse(userStr) : userStr
384
+          return String(u.userId || u.user_id || u.id || 1)
385
+        }
386
+      } catch (e) {}
387
+      return '1'
388
+    },
389
+
390
+    async sendMessage() {
391
+      const question = this.inputText.trim()
392
+      if (!question || this.loading) return
393
+
394
+      this.inputText = ''
395
+      this.clarifyQuestion = ''
396
+      this.loading = true
397
+
398
+      // 添加用户消息
399
+      this.messages.push({ role: 'user', text: question })
400
+
401
+      // 添加 AI 占位消息
402
+      const aiIdx = this.messages.length
403
+      this.messages.push({
404
+        role: 'assistant',
405
+        loading: true,
406
+        steps: [],
407
+        message: '',
408
+        sql: '',
409
+        columns: [],
410
+        rows: [],
411
+        answer: '',
412
+        sources: [],
413
+        showSql: false,
414
+      })
415
+      this.scrollToBottom()
416
+
417
+      try {
418
+        const response = await fetch(AI_URL, {
419
+          method: 'POST',
420
+          headers: { 'Content-Type': 'application/json' },
421
+          body: JSON.stringify({
422
+            question,
423
+            user_id: this.getUserId(),
424
+            llm_type: 'qwen',
425
+            session_id: this.sessionId,
426
+            history: this.history.slice(-3),
427
+          }),
428
+        })
429
+
430
+        if (!response.ok) throw new Error('请求失败 ' + response.status)
431
+
432
+        const reader = response.body.getReader()
433
+        const decoder = new TextDecoder()
434
+        let buffer = ''
435
+
436
+        while (true) {
437
+          const { done, value } = await reader.read()
438
+          if (done) break
439
+
440
+          buffer += decoder.decode(value, { stream: true })
441
+          const lines = buffer.split('\n')
442
+          buffer = lines.pop()
443
+
444
+          for (const line of lines) {
445
+            if (line.startsWith('data: ')) {
446
+              try {
447
+                const data = JSON.parse(line.slice(6))
448
+                this.handleEvent(aiIdx, data)
449
+              } catch (e) {}
450
+            }
451
+          }
452
+        }
453
+      } catch (err) {
454
+        this.$set(this.messages, aiIdx, {
455
+          ...this.messages[aiIdx],
456
+          loading: false,
457
+          isError: true,
458
+          message: '请求失败,请检查网络后重试',
459
+        })
460
+      }
461
+
462
+      this.loading = false
463
+      this.scrollToBottom()
464
+    },
465
+
466
+    handleEvent(aiIdx, data) {
467
+      const msg = this.messages[aiIdx]
468
+
469
+      if (data.step !== undefined && data.name) {
470
+        // 进度步骤
471
+        const steps = [...(msg.steps || [])]
472
+        const si = steps.findIndex(s => s.name === data.name)
473
+        const step = { name: data.name, status: data.status, duration: data.duration }
474
+        if (si >= 0) steps[si] = step
475
+        else steps.push(step)
476
+        this.$set(this.messages, aiIdx, { ...msg, steps })
477
+        return
478
+      }
479
+
480
+      if (data.question) {
481
+        // 意图澄清
482
+        this.clarifyQuestion = data.question
483
+        this.$set(this.messages, aiIdx, { ...msg, loading: false })
484
+        return
485
+      }
486
+
487
+      if (data.columns !== undefined || data.answer !== undefined) {
488
+        // 最终结果
489
+        this.$set(this.messages, aiIdx, {
490
+          ...msg,
491
+          loading: false,
492
+          message: data.message || '',
493
+          sql: data.sql || '',
494
+          columns: data.columns || [],
495
+          rows: data.rows || [],
496
+          answer: data.answer || '',
497
+          sources: data.sources || [],
498
+          isError: !data.success,
499
+          showSql: false,
500
+        })
501
+        // 记录历史
502
+        this.history.push({
503
+          question: (this.messages[aiIdx - 1] && this.messages[aiIdx - 1].text) || '',
504
+          sql: data.sql || '',
505
+        })
506
+        this.scrollToBottom()
507
+      }
508
+    },
509
+  },
510
+}
511
+</script>
512
+
513
+<style lang="scss" scoped>
514
+$primary: #1890ff;
515
+$bg: #f5f6fa;
516
+$bubble-user: #1890ff;
517
+$bubble-ai: #ffffff;
518
+$text-main: #333;
519
+$text-sub: #999;
520
+
521
+.ai-chat-page {
522
+  display: flex;
523
+  flex-direction: column;
524
+  height: 100vh;
525
+  background: $bg;
526
+  font-size: 28rpx;
527
+}
528
+
529
+/* 导航栏 */
530
+.nav-bar {
531
+  display: flex;
532
+  align-items: center;
533
+  padding: 20rpx 30rpx;
534
+  padding-top: calc(20rpx + env(safe-area-inset-top));
535
+  background: #fff;
536
+  border-bottom: 1rpx solid #eee;
537
+  position: sticky;
538
+  top: 0;
539
+  z-index: 10;
540
+}
541
+.back-btn { width: 80rpx; display: flex; align-items: center; }
542
+.back-icon { font-size: 50rpx; color: $primary; line-height: 1; }
543
+.nav-title { flex: 1; text-align: center; font-size: 32rpx; font-weight: 600; color: $text-main; }
544
+.nav-right { width: 80rpx; text-align: right; }
545
+.clear-btn { font-size: 26rpx; color: $text-sub; }
546
+
547
+/* 消息列表 */
548
+.msg-list {
549
+  flex: 1;
550
+  overflow: hidden;
551
+  padding: 20rpx 0;
552
+}
553
+
554
+/* 欢迎区 */
555
+.welcome {
556
+  display: flex;
557
+  flex-direction: column;
558
+  align-items: center;
559
+  padding: 80rpx 40rpx 40rpx;
560
+  gap: 16rpx;
561
+}
562
+.welcome-icon { width: 120rpx; height: 120rpx; border-radius: 24rpx; }
563
+.welcome-title { font-size: 32rpx; font-weight: 600; color: $text-main; }
564
+.welcome-sub { font-size: 26rpx; color: $text-sub; }
565
+.example-tags {
566
+  display: flex;
567
+  flex-wrap: wrap;
568
+  justify-content: center;
569
+  gap: 16rpx;
570
+  margin-top: 20rpx;
571
+}
572
+.tag {
573
+  padding: 12rpx 24rpx;
574
+  background: #fff;
575
+  border: 1rpx solid #d0e8ff;
576
+  border-radius: 40rpx;
577
+  font-size: 24rpx;
578
+  color: $primary;
579
+}
580
+
581
+/* 消息行 */
582
+.msg-row {
583
+  display: flex;
584
+  padding: 16rpx 24rpx;
585
+  gap: 16rpx;
586
+}
587
+.msg-user { flex-direction: row-reverse; }
588
+.msg-assistant { flex-direction: row; }
589
+
590
+/* 头像 */
591
+.avatar {
592
+  width: 72rpx;
593
+  height: 72rpx;
594
+  border-radius: 50%;
595
+  background: $primary;
596
+  display: flex;
597
+  align-items: center;
598
+  justify-content: center;
599
+  flex-shrink: 0;
600
+}
601
+.avatar-text { color: #fff; font-size: 22rpx; font-weight: 700; }
602
+
603
+/* 气泡包裹 */
604
+.bubble-wrap { flex: 1; display: flex; flex-direction: column; gap: 8rpx; max-width: 90%; }
605
+
606
+/* 气泡 */
607
+.bubble {
608
+  border-radius: 16rpx;
609
+  padding: 20rpx 24rpx;
610
+  word-break: break-all;
611
+}
612
+.bubble.user {
613
+  background: $bubble-user;
614
+  border-bottom-right-radius: 4rpx;
615
+  .bubble-text { color: #fff; }
616
+}
617
+.bubble.data, .bubble.knowledge {
618
+  background: $bubble-ai;
619
+  border-bottom-left-radius: 4rpx;
620
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,.06);
621
+}
622
+.bubble-text { line-height: 1.6; color: $text-main; }
623
+
624
+/* 进度步骤 */
625
+.steps {
626
+  display: flex;
627
+  flex-direction: column;
628
+  gap: 6rpx;
629
+  padding: 0 4rpx;
630
+}
631
+.step {
632
+  display: flex;
633
+  align-items: center;
634
+  gap: 8rpx;
635
+  font-size: 22rpx;
636
+  color: $text-sub;
637
+  &.done { color: #52c41a; }
638
+  &.running { color: $primary; }
639
+}
640
+.step-dot { font-size: 20rpx; width: 24rpx; }
641
+.step-time { font-size: 20rpx; }
642
+
643
+/* 结果状态 */
644
+.result-msg {
645
+  font-size: 24rpx;
646
+  color: $text-sub;
647
+  display: block;
648
+  margin-bottom: 12rpx;
649
+  &.error { color: #ff4d4f; }
650
+}
651
+
652
+/* 表格 */
653
+.data-table { margin-top: 8rpx; }
654
+.table-scroll { width: 100%; }
655
+.table-head, .table-row {
656
+  display: flex;
657
+  min-width: max-content;
658
+}
659
+.table-head { background: #f0f7ff; }
660
+.table-row.even { background: #fafafa; }
661
+.th, .td {
662
+  min-width: 120rpx;
663
+  padding: 12rpx 16rpx;
664
+  font-size: 24rpx;
665
+  white-space: nowrap;
666
+  border-bottom: 1rpx solid #f0f0f0;
667
+}
668
+.th { font-weight: 600; color: $primary; }
669
+.td { color: $text-main; }
670
+
671
+/* SQL 折叠 */
672
+.sql-block { margin-top: 12rpx; }
673
+.sql-toggle {
674
+  display: flex;
675
+  align-items: center;
676
+  gap: 8rpx;
677
+  padding: 8rpx 0;
678
+}
679
+.sql-label { font-size: 22rpx; color: $text-sub; background: #f0f0f0; padding: 2rpx 12rpx; border-radius: 6rpx; }
680
+.sql-arrow { font-size: 20rpx; color: $text-sub; }
681
+.sql-code {
682
+  background: #1e1e1e;
683
+  border-radius: 8rpx;
684
+  padding: 16rpx;
685
+  text { font-size: 22rpx; color: #d4d4d4; line-height: 1.5; }
686
+}
687
+
688
+/* 知识库来源 */
689
+.sources { margin-top: 12rpx; padding-top: 12rpx; border-top: 1rpx solid #f0f0f0; }
690
+.source-label { font-size: 22rpx; color: $text-sub; }
691
+.source-item { font-size: 22rpx; color: $primary; }
692
+
693
+/* 加载动画 */
694
+.loading { background: $bubble-ai !important; box-shadow: 0 2rpx 8rpx rgba(0,0,0,.06); }
695
+.dot-loader { display: flex; gap: 8rpx; align-items: center; height: 32rpx; }
696
+.dot {
697
+  width: 12rpx; height: 12rpx; border-radius: 50%; background: #ccc;
698
+  animation: bounce 1.2s infinite ease-in-out;
699
+  &:nth-child(2) { animation-delay: 0.2s; }
700
+  &:nth-child(3) { animation-delay: 0.4s; }
701
+}
702
+@keyframes bounce {
703
+  0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
704
+  40% { transform: scale(1); opacity: 1; }
705
+}
706
+
707
+/* 澄清提示 */
708
+.clarify-bar {
709
+  background: #fff7e6;
710
+  border-top: 1rpx solid #ffe58f;
711
+  padding: 16rpx 30rpx;
712
+}
713
+.clarify-text { font-size: 26rpx; color: #d46b08; }
714
+
715
+/* 输入栏 */
716
+.input-bar {
717
+  display: flex;
718
+  align-items: center;
719
+  padding: 16rpx 24rpx;
720
+  padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
721
+  background: #fff;
722
+  border-top: 1rpx solid #eee;
723
+  gap: 16rpx;
724
+  margin-bottom: 93rpx;
725
+}
726
+.input {
727
+  flex: 1;
728
+  height: 72rpx;
729
+  padding: 0 24rpx;
730
+  background: #f5f6fa;
731
+  border-radius: 36rpx;
732
+  font-size: 28rpx;
733
+  color: $text-main;
734
+  border: 1rpx solid #e8e8e8;
735
+}
736
+.send-btn {
737
+  width: 100rpx;
738
+  height: 72rpx;
739
+  background: $primary;
740
+  border-radius: 36rpx;
741
+  display: flex;
742
+  align-items: center;
743
+  justify-content: center;
744
+  &.disabled { background: #ccc; }
745
+}
746
+.send-text { color: #fff; font-size: 26rpx; }
747
+
748
+/* 麦克风按钮 */
749
+.mic-btn {
750
+  width: 72rpx;
751
+  height: 72rpx;
752
+  border-radius: 50%;
753
+  background: #f0f0f0;
754
+  display: flex;
755
+  align-items: center;
756
+  justify-content: center;
757
+  flex-shrink: 0;
758
+  transition: background 0.2s;
759
+  &.recording {
760
+    background: #ff4d4f;
761
+    animation: micPulse 0.8s infinite;
762
+  }
763
+}
764
+.mic-icon { font-size: 32rpx; line-height: 1; }
765
+
766
+/* 用阴影呼吸代替 scale,避免缩放触发 mouseleave */
767
+@keyframes micPulse {
768
+  0%, 100% { box-shadow: 0 0 0 0 rgba(255,77,79,0.5); }
769
+  50% { box-shadow: 0 0 0 12rpx rgba(255,77,79,0); }
770
+}
771
+
772
+/* 录音浮层 */
773
+.recording-overlay {
774
+  position: fixed;
775
+  inset: 0;
776
+  background: rgba(0,0,0,.5);
777
+  display: flex;
778
+  align-items: center;
779
+  justify-content: center;
780
+  z-index: 200;
781
+}
782
+.recording-inner {
783
+  background: #fff;
784
+  border-radius: 24rpx;
785
+  padding: 60rpx 80rpx;
786
+  display: flex;
787
+  flex-direction: column;
788
+  align-items: center;
789
+  gap: 20rpx;
790
+}
791
+.recording-icon { font-size: 80rpx; }
792
+.recording-tip { font-size: 26rpx; color: #666; }
793
+.recording-waves {
794
+  display: flex;
795
+  gap: 8rpx;
796
+  align-items: center;
797
+  height: 50rpx;
798
+}
799
+.wave {
800
+  width: 8rpx;
801
+  background: #1890ff;
802
+  border-radius: 4rpx;
803
+  animation: wave 0.8s infinite ease-in-out alternate;
804
+}
805
+@keyframes wave {
806
+  0%  { height: 10rpx; }
807
+  100%{ height: 50rpx; }
808
+}
809
+</style>

+ 14 - 5
src/pages/components/AreaDistribution.vue

@@ -46,22 +46,31 @@ export default {
46 46
       const option = {
47 47
         responsive: true,
48 48
         maintainAspectRatio: false,
49
+        tooltip: {
50
+          trigger: 'axis',
51
+          axisPointer: { type: 'shadow' }
52
+        },
53
+        legend: {
54
+          data: ['查获数量'],
55
+          textStyle: { color: '#666', fontSize: 10 },
56
+          top: 0
57
+        },
49 58
         grid: {
50 59
           left: '15%',
51 60
           right: '5%',
52 61
           bottom: '15%',
53
-          top: '10%'
62
+          top: '22%'
54 63
         },
55 64
         xAxis: {
56 65
           type: 'category',
57 66
           data: this.chartsData.labels,
58 67
           axisLine: {
59 68
             lineStyle: {
60
-              color: 'rgba(255, 255, 255, 0.1)'
69
+              color: 'rgba(0, 0, 0, 0.1)'
61 70
             }
62 71
           },
63 72
           axisLabel: {
64
-            color: 'rgba(255, 255, 255, 0.5)',
73
+            color: '#666',
65 74
             fontSize: 10,
66 75
             // rotate: 30
67 76
           }
@@ -76,11 +85,11 @@ export default {
76 85
           },
77 86
           splitLine: {
78 87
             lineStyle: {
79
-              color: 'rgba(255, 255, 255, 0.05)'
88
+              color: 'rgba(0, 0, 0, 0.05)'
80 89
             }
81 90
           },
82 91
           axisLabel: {
83
-            color: 'rgba(255, 255, 255, 0.5)',
92
+            color: '#666',
84 93
             fontSize: 10
85 94
           }
86 95
         },

+ 2 - 2
src/pages/components/AttendanceStatus.vue

@@ -39,13 +39,13 @@ export default {
39 39
   .attendance-number {
40 40
     font-size: 48rpx;
41 41
     font-weight: bold;
42
-    color: rgba(255, 255, 255, 0.95);
42
+    color: black;
43 43
   }
44 44
   
45 45
   .attendance-label {
46 46
     margin-top: 8rpx;
47 47
     font-size: 20rpx;
48
-    color: rgba(255, 255, 255, 0.5);
48
+    color: black;
49 49
     line-height: 1.4;
50 50
   }
51 51
 }

+ 35 - 16
src/pages/components/DailySeizureChart.vue

@@ -5,7 +5,7 @@
5 5
       <div class="chart-container" ref="totalChart"></div>
6 6
     </div>
7 7
     <div class="chart-section">
8
-      <div class="sub-title">部门对比</div>
8
+      <div class="sub-title">{{title}}</div>
9 9
       <div class="chart-container" ref="deptChart"></div>
10 10
     </div>
11 11
   </div>
@@ -17,6 +17,10 @@ import * as echarts from 'echarts'
17 17
 export default {
18 18
   name: 'DailySeizureChart',
19 19
   props: {
20
+    title: {
21
+      type: String,
22
+      default: '部门对比'
23
+    },
20 24
     chartsData: {
21 25
       type: Object,
22 26
       default: () => ({
@@ -73,23 +77,33 @@ export default {
73 77
       const option = {
74 78
         responsive: true,
75 79
         maintainAspectRatio: false,
80
+        tooltip: {
81
+          trigger: 'axis',
82
+          axisPointer: { type: 'shadow' }
83
+        },
84
+        legend: {
85
+          data: ['查获总数'],
86
+          textStyle: { color: '#666', fontSize: 10 },
87
+          top: 0
88
+        },
76 89
         grid: {
77 90
           left: '10%',
78 91
           right: '5%',
79
-          bottom: '14%',
80
-          top: '10%'
92
+          bottom: '18%',
93
+          top: '22%'
81 94
         },
82 95
         xAxis: {
83 96
           type: 'category',
84 97
           data: this.chartsData.total.labels,
85 98
           axisLine: {
86 99
             lineStyle: {
87
-              color: 'rgba(255, 255, 255, 0.1)'
100
+              color: 'rgba(0, 0, 0, 0.1)'
88 101
             }
89 102
           },
90 103
           axisLabel: {
91
-            color: 'rgba(255, 255, 255, 0.5)',
92
-            fontSize: 10
104
+            color: '#666',
105
+            fontSize: 10,
106
+            rotate: 30
93 107
           }
94 108
         },
95 109
         yAxis: {
@@ -102,11 +116,11 @@ export default {
102 116
           },
103 117
           splitLine: {
104 118
             lineStyle: {
105
-              color: 'rgba(255, 255, 255, 0.05)'
119
+              color: 'rgba(0, 0, 0, 0.05)'
106 120
             }
107 121
           },
108 122
           axisLabel: {
109
-            color: 'rgba(255, 255, 255, 0.5)',
123
+            color: '#666',
110 124
             fontSize: 10
111 125
           }
112 126
         },
@@ -152,10 +166,15 @@ export default {
152 166
       const option = {
153 167
         responsive: true,
154 168
         maintainAspectRatio: false,
169
+        tooltip: {
170
+          trigger: 'axis',
171
+          axisPointer: { type: 'shadow' }
172
+        },
155 173
         legend: {
174
+          type:'scroll',
156 175
           data: (this.chartsData.dept.deptData || []).map(d => d.name),
157 176
           textStyle: {
158
-            color: 'rgba(255, 255, 255, 0.5)',
177
+            color: '#333',
159 178
             fontSize: 9
160 179
           },
161 180
           top: 0
@@ -175,7 +194,7 @@ export default {
175 194
             }
176 195
           },
177 196
           axisLabel: {
178
-            color: 'rgba(255, 255, 255, 0.5)',
197
+            color: '#333',
179 198
             fontSize: 10
180 199
           }
181 200
         },
@@ -193,7 +212,7 @@ export default {
193 212
             }
194 213
           },
195 214
           axisLabel: {
196
-            color: 'rgba(255, 255, 255, 0.5)',
215
+            color: '#333',
197 216
             fontSize: 10
198 217
           }
199 218
         },
@@ -214,20 +233,20 @@ export default {
214 233
 .daily-seizure-chart {
215 234
   .chart-section {
216 235
     margin-bottom: 32rpx;
217
-    
236
+
218 237
     &:last-child {
219 238
       margin-bottom: 0;
220 239
     }
221 240
   }
222
-  
241
+
223 242
   .sub-title {
224 243
     font-size: 24rpx;
225
-    color: rgba(255, 255, 255, 0.5);
244
+    color: #666;
226 245
     margin-bottom: 16rpx;
227 246
   }
228
-  
247
+
229 248
   .chart-container {
230
-    height:400rpx;
249
+    height:450rpx;
231 250
   }
232 251
 }
233 252
 </style>

+ 6 - 6
src/pages/components/DeptSelector.vue

@@ -99,7 +99,7 @@ export default {
99 99
 
100 100
 <style lang="scss" scoped>
101 101
 .dept-picker {
102
-    background: #1E1B4B;
102
+    background: #fff;
103 103
     display: flex;
104 104
     flex-direction: column;
105 105
     border-radius: 16rpx 16rpx 0 0;
@@ -109,7 +109,7 @@ export default {
109 109
 
110 110
 .picker-header {
111 111
     padding: 24rpx 32rpx;
112
-    border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
112
+    border-bottom: 1rpx solid #eee;
113 113
     display: flex;
114 114
     justify-content: center;
115 115
     flex-shrink: 0;
@@ -118,7 +118,7 @@ export default {
118 118
 .picker-title {
119 119
     font-size: 32rpx;
120 120
     font-weight: 600;
121
-    color: #fff;
121
+    color: #333;
122 122
 }
123 123
 
124 124
 .search-box {
@@ -138,12 +138,12 @@ export default {
138 138
     align-items: center;
139 139
     justify-content: space-between;
140 140
     padding: 24rpx 0;
141
-    border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
141
+    border-bottom: 1rpx solid #f0f0f0;
142 142
 }
143 143
 
144 144
 .dept-item-name {
145 145
     font-size: 28rpx;
146
-    color: rgba(255, 255, 255, 0.9);
146
+    color: #333;
147 147
 }
148 148
 
149 149
 .empty-state,
@@ -152,7 +152,7 @@ export default {
152 152
     display: flex;
153 153
     justify-content: center;
154 154
     align-items: center;
155
-    color: rgba(255, 255, 255, 0.4);
155
+    color: #999;
156 156
     font-size: 24rpx;
157 157
 }
158 158
 </style>

+ 8 - 8
src/pages/components/DeptStats.vue

@@ -55,11 +55,12 @@ export default {
55 55
     align-items: center;
56 56
     justify-content: center;
57 57
     padding: 24rpx 16rpx;
58
-    background: linear-gradient(135deg, rgba(33, 33, 58, 0.95), rgba(15, 70, 250, 0.2));
58
+    background: linear-gradient(135deg, #f5f7fa, #e8ecf1);
59 59
     border-radius: 16rpx;
60
-    border: 1rpx solid rgba(159, 3, 193, 0.3);
60
+    border: 1rpx solid #e0e0e0;
61 61
     position: relative;
62 62
     overflow: hidden;
63
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
63 64
 
64 65
     &::before {
65 66
         content: '';
@@ -68,7 +69,7 @@ export default {
68 69
         left: 0;
69 70
         right: 0;
70 71
         height: 2rpx;
71
-        background: linear-gradient(90deg, #0f46fa, transparent);
72
+        background: linear-gradient(90deg, #60A5FA, transparent);
72 73
     }
73 74
 
74 75
     &::after {
@@ -78,21 +79,20 @@ export default {
78 79
         right: 0;
79 80
         left: 0;
80 81
         height: 2rpx;
81
-        background: linear-gradient(90deg, transparent, #9903C1);
82
+        background: linear-gradient(90deg, transparent, #A78BFA);
82 83
     }
83 84
 }
84 85
 
85 86
 .stat-value {
86 87
     font-size: 36rpx;
87 88
     font-weight: bold;
88
-    color: #fff;
89
-    text-shadow: 0 0 12rpx rgba(15, 70, 250, 0.8);
89
+    color: #333;
90 90
     line-height: 1;
91 91
 }
92 92
 
93 93
 .stat-label {
94 94
     font-size: 20rpx;
95
-    color: #a0c4ff;
95
+    color: #666;
96 96
     margin-top: 8rpx;
97 97
 }
98 98
 
@@ -102,7 +102,7 @@ export default {
102 102
     .dept-name {
103 103
         font-size: 28rpx;
104 104
         font-weight: bold;
105
-        color: #fff;
105
+        color: #333;
106 106
         text-align: center;
107 107
     }
108 108
 }

+ 2 - 2
src/pages/components/DutyInfo.vue

@@ -38,12 +38,12 @@ export default {
38 38
   
39 39
   .duty-label {
40 40
     font-size: 24rpx;
41
-    color: rgba(255, 255, 255, 0.5);
41
+    color: black;
42 42
   }
43 43
   
44 44
   .duty-value {
45 45
     font-size: 24rpx;
46
-    color: rgba(255, 255, 255, 0.9);
46
+    color: black;
47 47
   }
48 48
 }
49 49
 </style>

+ 125 - 0
src/pages/components/EmployeeTreeNode.vue

@@ -0,0 +1,125 @@
1
+<template>
2
+    <view class="tree-node">
3
+        <view 
4
+            :class="['node-row', { 'is-dept': isDept, 'is-user': isUser }]"
5
+            @click="handleClick"
6
+        >
7
+            <view class="toggle-icon" v-if="isDept">
8
+                <u-icon :name="expanded ? 'arrow-down' : 'arrow-right'" size="16" color="#999" />
9
+            </view>
10
+            <view class="toggle-icon" v-else>
11
+                <text class="user-dot">●</text>
12
+            </view>
13
+            <text class="node-label">{{ nodeLabel }}</text>
14
+            <u-icon v-if="isUser && nodeId === selectedId" name="checkmark" color="#34D399" size="18" />
15
+        </view>
16
+        <view v-if="isDept && expanded" class="node-children">
17
+            <template v-for="child in nodeChildren">
18
+                <employee-tree-node
19
+                    :key="child.id"
20
+                    :node="child"
21
+                    :expanded-ids="expandedIds"
22
+                    :selected-id="selectedId"
23
+                    @toggle="$emit('toggle', $event)"
24
+                    @select="$emit('select', $event)"
25
+                />
26
+            </template>
27
+        </view>
28
+    </view>
29
+</template>
30
+
31
+<script>
32
+export default {
33
+    name: 'EmployeeTreeNode',
34
+    props: {
35
+        node: {
36
+            type: Object,
37
+            required: true
38
+        },
39
+        expandedIds: {
40
+            type: [Array, Set],
41
+            default: () => []
42
+        },
43
+        selectedId: {
44
+            type: [String, Number],
45
+            default: null
46
+        }
47
+    },
48
+    computed: {
49
+        isDept() {
50
+            const hasChildren = this.node.children && this.node.children.length > 0
51
+            return hasChildren || this.node.nodeType === 'dept'
52
+        },
53
+        isUser() {
54
+            return !this.isDept
55
+        },
56
+        nodeId() {
57
+            return this.node.userId || this.node.id
58
+        },
59
+        nodeLabel() {
60
+            return this.node.nickName || this.node.label || this.node.userName || this.node.name || ''
61
+        },
62
+        nodeChildren() {
63
+            return this.node.children || []
64
+        },
65
+        expanded() {
66
+            if (this.expandedIds instanceof Set) {
67
+                return this.expandedIds.has(this.node.id)
68
+            }
69
+            return this.expandedIds.includes(this.node.id)
70
+        }
71
+    },
72
+    methods: {
73
+        handleClick() {
74
+            if (this.isDept) {
75
+                this.$emit('toggle', this.node.id)
76
+            } else {
77
+                this.$emit('select', {
78
+                    userId: this.nodeId,
79
+                    nickName: this.nodeLabel
80
+                })
81
+            }
82
+        }
83
+    }
84
+}
85
+</script>
86
+
87
+<style lang="scss" scoped>
88
+.tree-node {
89
+    width: 100%;
90
+}
91
+
92
+.node-row {
93
+    display: flex;
94
+    align-items: center;
95
+    padding: 20rpx 32rpx 20rpx 16rpx;
96
+    gap: 12rpx;
97
+
98
+    &.is-dept {
99
+        padding-left: 16rpx;
100
+    }
101
+}
102
+
103
+.toggle-icon {
104
+    width: 40rpx;
105
+    height: 40rpx;
106
+    display: flex;
107
+    align-items: center;
108
+    justify-content: center;
109
+}
110
+
111
+.user-dot {
112
+    font-size: 16rpx;
113
+    color: #999;
114
+}
115
+
116
+.node-label {
117
+    flex: 1;
118
+    font-size: 28rpx;
119
+    color: #333;
120
+}
121
+
122
+.node-children {
123
+    padding-left: 32rpx;
124
+}
125
+</style>

+ 7 - 7
src/pages/components/GroupMemberTable.vue

@@ -52,25 +52,25 @@ export default {
52 52
   .table-scroll {
53 53
     width: 100%;
54 54
   }
55
-  
55
+
56 56
   .data-table {
57 57
     width: 100%;
58 58
     font-size: 24rpx;
59 59
     border-collapse: collapse;
60
-    
60
+
61 61
     th, td {
62 62
       padding: 16rpx 8rpx;
63 63
       text-align: left;
64 64
       white-space: nowrap;
65 65
     }
66
-    
66
+
67 67
     th {
68
-      color: rgba(255, 255, 255, 0.5);
69
-      border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
68
+      color: #666;
69
+      border-bottom: 1rpx solid #e0e0e0;
70 70
     }
71
-    
71
+
72 72
     td {
73
-      color: rgba(255, 255, 255, 0.9);
73
+      color: #333;
74 74
     }
75 75
   }
76 76
 }

+ 11 - 8
src/pages/components/InterceptionDistribution.vue

@@ -45,11 +45,16 @@ export default {
45 45
             const option = {
46 46
                 responsive: true,
47 47
                 maintainAspectRatio: false,
48
+                legend: {
49
+                    data: ['拦截数量'],
50
+                    textStyle: { color: '#666', fontSize: 10 },
51
+                    top: 0
52
+                },
48 53
                 grid: {
49 54
                     left: '25%',
50 55
                     right: '10%',
51 56
                     bottom: '10%',
52
-                    top: '10%'
57
+                    top: '22%'
53 58
                 },
54 59
                 xAxis: {
55 60
                     type: 'value',
@@ -57,11 +62,11 @@ export default {
57 62
                     axisTick: { show: false },
58 63
                     splitLine: {
59 64
                         lineStyle: {
60
-                            color: 'rgba(255, 255, 255, 0.05)'
65
+                            color: 'rgba(0, 0, 0, 0.05)'
61 66
                         }
62 67
                     },
63 68
                     axisLabel: {
64
-                        color: 'rgba(255, 255, 255, 0.5)',
69
+                        color: '#666',
65 70
                         fontSize: 9
66 71
                     }
67 72
                 },
@@ -72,11 +77,11 @@ export default {
72 77
                     axisTick: { show: false },
73 78
                     splitLine: {
74 79
                         lineStyle: {
75
-                            color: 'rgba(255, 255, 255, 0.05)'
80
+                            color: 'rgba(0, 0, 0, 0.05)'
76 81
                         }
77 82
                     },
78 83
                     axisLabel: {
79
-                        color: 'rgba(255, 255, 255, 0.6)',
84
+                        color: '#666',
80 85
                         fontSize: 9,
81 86
                         formatter: function (value) {
82 87
                             return value.length > 5 ? value.substring(0, 6) + '...' : value
@@ -105,9 +110,7 @@ export default {
105 110
                 ],
106 111
                 tooltip: {
107 112
                     trigger: 'axis',
108
-                    backgroundColor: 'rgba(13,80,122,0.95)',
109
-                    borderColor: '#70CFE7',
110
-                    textStyle: { color: '#fff' }
113
+                   axisPointer: { type: 'shadow' }
111 114
                 }
112 115
             }
113 116
             this.chart.setOption(option)

+ 20 - 10
src/pages/components/ItemDistribution.vue

@@ -1,12 +1,12 @@
1 1
 <template>
2 2
   <div class="item-distribution">
3 3
     <div class="chart-container" ref="itemChart"></div>
4
-    <div class="legend-grid">
4
+    <!-- <div class="legend-grid">
5 5
       <div v-for="(item, index) in chartsData.items" :key="index" class="legend-item">
6 6
         <div class="legend-dot" :style="{ backgroundColor: item.color }"></div>
7 7
         <div class="legend-text">{{ item.name }}: {{ item.value }}</div>
8 8
       </div>
9
-    </div>
9
+    </div> -->
10 10
   </div>
11 11
 </template>
12 12
 
@@ -53,9 +53,19 @@ export default {
53 53
       const option = {
54 54
         responsive: true,
55 55
         maintainAspectRatio: false,
56
+        tooltip: {
57
+          trigger: 'item',
58
+          formatter: '{b}: {c} ({d}%)'
59
+        },
60
+        legend: {
61
+          type: 'scroll',
62
+          data: this.chartsData.items.map(item => item.name),
63
+          textStyle: { color: '#666', fontSize: 10 },
64
+          top: 0
65
+        },
56 66
         series: [{
57 67
           type: 'pie',
58
-          radius: ['50%', '70%'],
68
+          radius: ['30%', '47%'],
59 69
           data: this.chartsData.items.map(item => ({
60 70
             name: item.name,
61 71
             value: item.value,
@@ -68,7 +78,7 @@ export default {
68 78
           },
69 79
           label: {
70 80
             show: true,
71
-            color: 'rgba(255, 255, 255, 0.7)',
81
+            color: '#333',
72 82
             fontSize: 10,
73 83
             formatter: '{b}\n{d}%'
74 84
           }
@@ -86,30 +96,30 @@ export default {
86 96
 <style lang="scss" scoped>
87 97
 .item-distribution {
88 98
   .chart-container {
89
-    height: 300rpx;
99
+    height: 450rpx;
90 100
   }
91
-  
101
+
92 102
   .legend-grid {
93 103
     display: grid;
94 104
     grid-template-columns: repeat(2, 1fr);
95 105
     gap: 8rpx;
96 106
     margin-top: 16rpx;
97 107
   }
98
-  
108
+
99 109
   .legend-item {
100 110
     display: flex;
101 111
     align-items: center;
102 112
     font-size: 24rpx;
103
-    
113
+
104 114
     .legend-dot {
105 115
       width: 12rpx;
106 116
       height: 12rpx;
107 117
       border-radius: 50%;
108 118
       margin-right: 8rpx;
109 119
     }
110
-    
120
+
111 121
     .legend-text {
112
-      color: rgba(255, 255, 255, 0.7);
122
+      color: #666;
113 123
     }
114 124
   }
115 125
 }

+ 45 - 17
src/pages/components/MemberBasicDistribution.vue

@@ -71,9 +71,18 @@ export default {
71 71
       const option = {
72 72
         responsive: true,
73 73
         maintainAspectRatio: false,
74
+        tooltip: {
75
+          trigger: 'item',
76
+          formatter: '{b}: {c} ({d}%)'
77
+        },
78
+        legend: {
79
+          data: this.chartsData.gender.labels,
80
+          textStyle: { color: '#666', fontSize: 10 },
81
+          top: 0
82
+        },
74 83
         series: [{
75 84
           type: 'pie',
76
-          radius: ['40%', '60%'],
85
+          radius: ['25%', '40%'],
77 86
           data: this.chartsData.gender.data.map((value, index) => ({
78 87
             name: this.chartsData.gender.labels[index],
79 88
             value: value
@@ -86,7 +95,7 @@ export default {
86 95
           },
87 96
           label: {
88 97
             show: true,
89
-            color: 'rgba(255, 255, 255, 0.7)',
98
+            color: '#333',
90 99
             fontSize: 10,
91 100
             formatter: '{b}\n{d}%'
92 101
           }
@@ -100,22 +109,32 @@ export default {
100 109
       const option = {
101 110
         responsive: true,
102 111
         maintainAspectRatio: false,
112
+        tooltip: {
113
+          trigger: 'item',
114
+          formatter: '{b}: {c} ({d}%)'
115
+        },
116
+        legend: {
117
+          type: 'scroll',
118
+          data: this.chartsData.ethnicity.labels,
119
+          textStyle: { color: '#666', fontSize: 10 },
120
+          top: 0
121
+        },
103 122
         series: [{
104 123
           type: 'pie',
105
-          radius: ['40%', '60%'],
124
+          radius: ['25%', '40%'],
106 125
           data: this.chartsData.ethnicity.data.map((value, index) => ({
107 126
             name: this.chartsData.ethnicity.labels[index],
108 127
             value: value
109 128
           })),
110
-          itemStyle: {
111
-            color: function(params) {
112
-              const colors = ['#A78BFA', '#34D399']
113
-              return colors[params.dataIndex]
114
-            }
115
-          },
129
+          // itemStyle: {
130
+          //   color: function(params) {
131
+          //     const colors = ['#A78BFA', '#34D399']
132
+          //     return colors[params.dataIndex]
133
+          //   }
134
+          // },
116 135
           label: {
117 136
             show: true,
118
-            color: 'rgba(255, 255, 255, 0.7)',
137
+            color: '#333',
119 138
             fontSize: 10,
120 139
             formatter: '{b}\n{d}%'
121 140
           }
@@ -129,9 +148,18 @@ export default {
129 148
       const option = {
130 149
         responsive: true,
131 150
         maintainAspectRatio: false,
151
+        tooltip: {
152
+          trigger: 'item',
153
+          formatter: '{b}: {c} ({d}%)'
154
+        },
155
+        legend: {
156
+          data: this.chartsData.political.labels,
157
+          textStyle: { color: '#666', fontSize: 10 },
158
+          top: 0
159
+        },
132 160
         series: [{
133 161
           type: 'pie',
134
-          radius: ['40%', '60%'],
162
+          radius: ['25%', '40%'],
135 163
           data: this.chartsData.political.data.map((value, index) => ({
136 164
             name: this.chartsData.political.labels[index],
137 165
             value: value
@@ -144,7 +172,7 @@ export default {
144 172
           },
145 173
           label: {
146 174
             show: true,
147
-            color: 'rgba(255, 255, 255, 0.7)',
175
+            color: '#333',
148 176
             fontSize: 10,
149 177
             formatter: '{b}\n{d}%'
150 178
           }
@@ -168,22 +196,22 @@ export default {
168 196
     flex-direction: column;
169 197
     gap: 32rpx;
170 198
   }
171
-  
199
+
172 200
   .chart-item {
173 201
     display: flex;
174 202
     align-items: center;
175 203
     gap: 24rpx;
176
-    
204
+
177 205
     .chart-label {
178 206
       width: 160rpx;
179 207
       flex-shrink: 0;
180 208
       font-size: 24rpx;
181
-      color: rgba(255, 255, 255, 0.5);
209
+      color: #666;
182 210
     }
183
-    
211
+
184 212
     .chart-container {
185 213
       flex: 1;
186
-      height:250rpx;
214
+      height:450rpx;
187 215
     }
188 216
   }
189 217
 }

+ 66 - 30
src/pages/components/MemberPositionDistribution.vue

@@ -69,23 +69,33 @@ export default {
69 69
       const option = {
70 70
         responsive: true,
71 71
         maintainAspectRatio: false,
72
+        tooltip: {
73
+          trigger: 'axis',
74
+          axisPointer: { type: 'shadow' }
75
+        },
76
+        legend: {
77
+          data: ['职业资格等级'],
78
+          textStyle: { color: '#666', fontSize: 10 },
79
+          top: 0
80
+        },
72 81
         grid: {
73 82
           left: '10%',
74 83
           right: '5%',
75 84
           bottom: '14%',
76
-          top: '10%'
85
+          top: '22%'
77 86
         },
78 87
         xAxis: {
79 88
           type: 'category',
80 89
           data: this.chartsData.qualification.labels,
81 90
           axisLine: {
82 91
             lineStyle: {
83
-              color: 'rgba(255, 255, 255, 0.1)'
92
+              color: 'rgba(0, 0, 0, 0.1)'
84 93
             }
85 94
           },
86 95
           axisLabel: {
87
-            color: 'rgba(255, 255, 255, 0.5)',
88
-            fontSize: 10
96
+            color: '#666',
97
+            fontSize: 10,
98
+            rotate: 30
89 99
           }
90 100
         },
91 101
         yAxis: {
@@ -98,15 +108,17 @@ export default {
98 108
           },
99 109
           splitLine: {
100 110
             lineStyle: {
101
-              color: 'rgba(255, 255, 255, 0.05)'
111
+              color: 'rgba(0, 0, 0, 0.05)'
102 112
             }
103 113
           },
104 114
           axisLabel: {
105
-            color: 'rgba(255, 255, 255, 0.5)',
106
-            fontSize: 10
115
+            color: '#666',
116
+            fontSize: 10,
117
+            rotate: 30
107 118
           }
108 119
         },
109 120
         series: [{
121
+          name: '职业资格等级',
110 122
           type: 'bar',
111 123
           data: this.chartsData.qualification.data,
112 124
           itemStyle: {
@@ -134,23 +146,34 @@ export default {
134 146
       const option = {
135 147
         responsive: true,
136 148
         maintainAspectRatio: false,
149
+        tooltip: {
150
+          trigger: 'axis',
151
+          axisPointer: { type: 'shadow' }
152
+        },
153
+        legend: {
154
+          show: true,
155
+          data: ['开机年限'],
156
+          textStyle: { color: '#333', fontSize: 10 },
157
+          top: 0
158
+        },
137 159
         grid: {
138 160
           left: '10%',
139 161
           right: '5%',
140 162
           bottom: '14%',
141
-          top: '10%'
163
+          top: '22%'
142 164
         },
143 165
         xAxis: {
144 166
           type: 'category',
145 167
           data: this.chartsData.experience.labels,
146 168
           axisLine: {
147 169
             lineStyle: {
148
-              color: 'rgba(255, 255, 255, 0.1)'
170
+              color: 'rgba(0, 0, 0, 0.1)'
149 171
             }
150 172
           },
151 173
           axisLabel: {
152
-            color: 'rgba(255, 255, 255, 0.5)',
153
-            fontSize: 10
174
+            color: '#666',
175
+            fontSize: 10,
176
+            rotate: 30
154 177
           }
155 178
         },
156 179
         yAxis: {
@@ -163,15 +186,17 @@ export default {
163 186
           },
164 187
           splitLine: {
165 188
             lineStyle: {
166
-              color: 'rgba(255, 255, 255, 0.05)'
189
+              color: 'rgba(0, 0, 0, 0.05)'
167 190
             }
168 191
           },
169 192
           axisLabel: {
170
-            color: 'rgba(255, 255, 255, 0.5)',
171
-            fontSize: 10
193
+            color: '#666',
194
+            fontSize: 10,
195
+            rotate: 30
172 196
           }
173 197
         },
174 198
         series: [{
199
+          name: '开机年限',
175 200
           type: 'bar',
176 201
           data: this.chartsData.experience.data,
177 202
           itemStyle: {
@@ -199,25 +224,34 @@ export default {
199 224
       const option = {
200 225
         responsive: true,
201 226
         maintainAspectRatio: false,
227
+        tooltip: {
228
+          trigger: 'axis',
229
+          axisPointer: { type: 'shadow' }
230
+        },
231
+        legend: {
232
+          data: ['岗位资质'],
233
+          textStyle: { color: '#666', fontSize: 10 },
234
+          top: 0
235
+        },
202 236
         grid: {
203 237
           left: '10%',
204 238
           right: '5%',
205 239
           bottom: '20%',
206
-          top: '10%'
240
+          top: '22%'
207 241
         },
208 242
         xAxis: {
209 243
           type: 'category',
210 244
           data: this.chartsData.position.labels,
211 245
           axisLine: {
212 246
             lineStyle: {
213
-              color: 'rgba(255, 255, 255, 0.1)'
247
+              color: 'rgba(0, 0, 0, 0.1)'
214 248
             }
215 249
           },
216 250
           axisLabel: {
217
-            color: 'rgba(255, 255, 255, 0.5)',
251
+            color: '#666',
218 252
             fontSize: 10,
219 253
             rotate: 30,
220
-            margin: 15
254
+         
221 255
           }
222 256
         },
223 257
         yAxis: {
@@ -230,12 +264,13 @@ export default {
230 264
           },
231 265
           splitLine: {
232 266
             lineStyle: {
233
-              color: 'rgba(255, 255, 255, 0.05)'
267
+              color: 'rgba(0, 0, 0, 0.05)'
234 268
             }
235 269
           },
236 270
           axisLabel: {
237
-            color: 'rgba(255, 255, 255, 0.5)',
238
-            fontSize: 10
271
+            color: '#666',
272
+            fontSize: 10,
273
+            rotate: 30
239 274
           }
240 275
         },
241 276
         dataZoom: [{
@@ -244,17 +279,18 @@ export default {
244 279
           xAxisIndex: 0,
245 280
           height: 20,
246 281
           bottom: 0,
247
-          borderColor: 'rgba(255, 255, 255, 0.1)',
248
-          backgroundColor: 'rgba(45, 42, 85, 0.8)',
249
-          fillerColor: 'rgba(167, 139, 250, 0.3)',
250
-          handleStyle: {
251
-            color: '#A78BFA'
252
-          },
253
-          labelStyle: {
254
-            color: 'rgba(255, 255, 255, 0.5)'
255
-          }
282
+          // borderColor: 'gray',
283
+          // backgroundColor: 'gray',
284
+          // fillerColor: 'gray',
285
+          // handleStyle: {
286
+          //   color: '#A78BFA'
287
+          // },
288
+          // labelStyle: {
289
+          //   color: 'rgba(255, 255, 255, 0.5)'
290
+          // }
256 291
         }],
257 292
         series: [{
293
+          name: '岗位资质',
258 294
           type: 'bar',
259 295
           data: this.chartsData.position.data,
260 296
           itemStyle: {

+ 9 - 11
src/pages/components/PassengerChart.vue

@@ -82,13 +82,11 @@ export default {
82 82
       const option = {
83 83
         tooltip: {
84 84
           trigger: 'axis',
85
-          backgroundColor: 'rgba(30, 27, 75, 0.95)',
86
-          borderColor: '#A78BFA',
87
-          textStyle: { color: '#fff' }
85
+   axisPointer: { type: 'shadow' }
88 86
         },
89 87
         legend: {
90 88
           data: [...areaNames, '人流总数'],
91
-          textStyle: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
89
+          textStyle: { color: '#666', fontSize: 10 },
92 90
           top: 0
93 91
         },
94 92
         grid: {
@@ -100,27 +98,27 @@ export default {
100 98
         xAxis: {
101 99
           type: 'category',
102 100
           data: this.chartsData.labels,
103
-          axisLabel: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
104
-          axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } }
101
+          axisLabel: { color: '#666', fontSize: 10 },
102
+          axisLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.1)' } }
105 103
         },
106 104
         yAxis: [
107 105
           {
108 106
             type: 'value',
109 107
             name: '人数',
110
-            nameTextStyle: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
108
+            nameTextStyle: { color: '#666', fontSize: 10 },
111 109
             position: 'left',
112 110
             max: Math.ceil(maxVal * 1.2),
113
-            axisLabel: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
111
+            axisLabel: { color: '#666', fontSize: 10 },
114 112
             axisLine: { show: false },
115
-            splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }
113
+            splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.05)' } }
116 114
           },
117 115
           {
118 116
             type: 'value',
119 117
             name: '人流总数',
120
-            nameTextStyle: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
118
+            nameTextStyle: { color: '#666', fontSize: 10 },
121 119
             position: 'right',
122 120
             max: Math.ceil(maxVal * 1.2),
123
-            axisLabel: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
121
+            axisLabel: { color: '#666', fontSize: 10 },
124 122
             axisLine: { show: false },
125 123
             splitLine: { show: false }
126 124
           }

+ 109 - 56
src/pages/components/ProfileRadar.vue

@@ -4,6 +4,12 @@
4 4
             <text>暂无数据</text>
5 5
         </view>
6 6
         <template v-else>
7
+            <div class="chart-legend">
8
+                <div class="legend-item legend-warning"><span></span>预警线(低于75分)</div>
9
+                <!-- <div class="legend-item legend-normal"><span></span>正常线(75~90分)</div> -->
10
+                <div class="legend-item legend-excellent"><span></span>优秀线(高于90分)</div>
11
+                <div class="legend-item legend-current"><span></span>当前分值</div>
12
+            </div>
7 13
             <div class="chart-container" ref="radarChart"></div>
8 14
             <view class="radar-list">
9 15
                 <view v-for="(item, index) in radarData" :key="index" class="radar-item">
@@ -92,77 +98,66 @@ export default {
92 98
                 this.chart.dispose()
93 99
                 this.chart = null
94 100
             }
95
-            
101
+
96 102
             // 没有数据时不渲染图表
97 103
             if (!this.radarData || this.radarData.length === 0) {
98 104
                 return
99 105
             }
100
-            
101
-            const values = this.radarData.map(item => {
102
-                if (typeof item === 'object' && item !== null) {
103
-                    return item.finalScore || item.score || 0
104
-                }
105
-                return 0
106
-            })
107
-            
106
+
107
+
108
+
109
+
108 110
             this.chart = echarts.init(this.$refs.radarChart)
109
-            
110
-            const option = {
111
-                responsive: true,
112
-                maintainAspectRatio: false,
113
-                radar: {
114
-                    indicator: this.indicators,
115
-                    center: ['50%', '50%'],
116
-                    radius: '60%',
117
-                    splitNumber: 5,
118
-                    axisLine: {
119
-                        lineStyle: {
120
-                            color: 'rgba(255, 255, 255, 0.2)'
121
-                        }
111
+            const radarData = {
112
+                grounp: this.radarData.map(d => ({ name: d.name + '\n\n' + d.finalScore, max: 100 })),
113
+                data: [{
114
+                    name: '个人能力',
115
+                    value: this.radarData.map(item => item.finalScore || 0),
116
+                    symbolSize: 10,
117
+                    areaStyle: {
118
+                        show: false,
119
+                        opacity: 0
122 120
                     },
123
-                    splitLine: {
124
-                        lineStyle: {
125
-                            color: 'rgba(255, 255, 255, 0.1)'
126
-                        }
121
+                    lineStyle: {
122
+                        color: '#4DC8FE',
123
+                        width: 1
127 124
                     },
125
+                    itemStyle: { color: '#fff', borderWidth: 1, borderColor: '#00C8DA', borderJoin: 'round' }
126
+                }]
127
+            }
128
+
129
+            const option = {
130
+                radar: {
131
+                    indicator: radarData.grounp,
132
+                    center: ['50%', '52%'],
133
+                    radius: '50%',
134
+                    splitNumber: 8,
135
+                    axisLine: { lineStyle: { color: '#ccc' } },
136
+                    splitLine: { lineStyle: { color: ['#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#fe4322', '#8EC742', '#ccc'], width: 1 } },
128 137
                     splitArea: { show: false },
129 138
                     axisName: {
130
-                        color: 'rgba(255, 255, 255, 0.6)',
131
-                        fontSize: 10
139
+                        color: '#333',
140
+                        fontSize: 12
132 141
                     }
133 142
                 },
134 143
                 series: [
135 144
                     {
136 145
                         type: 'radar',
146
+                        data: radarData.data,
137 147
                         symbol: 'circle',
138
-                        symbolSize: 8,
139
-                        data: [
140
-                            {
141
-                                value: values,
142
-                                name: '综合得分',
143
-                                lineStyle: {
144
-                                    color: '#4DC8FE',
145
-                                    width: 2
146
-                                },
147
-                                itemStyle: {
148
-                                    color: '#fff',
149
-                                    borderWidth: 1,
150
-                                    borderColor: '#00C8DA'
151
-                                },
152
-                                areaStyle: {
153
-                                    color: 'rgba(77, 200, 254, 0.2)'
154
-                                }
155
-                            }
156
-                        ]
148
+                        symbolSize: 13,
149
+                        label: {
150
+                            show: false,
151
+                            formatter: (p) => p.value,
152
+                            color: '#333',
153
+                            fontSize: 16,
154
+                            fontWeight: 'bold'
155
+                        }
157 156
                     }
158 157
                 ],
159 158
                 tooltip: {
160 159
                     trigger: 'item',
161
-                    backgroundColor: 'rgba(13, 80, 122, 0.95)',
162
-                    borderColor: '#70CFE7',
163
-                    textStyle: {
164
-                        color: '#fff'
165
-                    }
160
+                    axisPointer: { type: 'shadow' }
166 161
                 }
167 162
             }
168 163
             this.chart.setOption(option)
@@ -186,7 +181,7 @@ export default {
186 181
         display: flex;
187 182
         align-items: center;
188 183
         justify-content: center;
189
-        color: rgba(255, 255, 255, 0.5);
184
+        color: #999;
190 185
         font-size: 28rpx;
191 186
     }
192 187
 
@@ -195,6 +190,63 @@ export default {
195 190
         height: 350rpx;
196 191
     }
197 192
 
193
+    .chart-legend {
194
+        display: flex;
195
+        justify-content: center;
196
+        align-items: center;
197
+        flex-wrap: wrap;
198
+        gap: 10px 18px;
199
+        padding-top: 8px;
200
+        color: #fff;
201
+        font-size: 14px;
202
+
203
+    }
204
+
205
+    .legend-item {
206
+        display: flex;
207
+        align-items: center;
208
+        gap: 6rpx;
209
+
210
+        span {
211
+            width: 24rpx;
212
+            height: 10rpx;
213
+            border: 2rpx solid currentColor;
214
+            border-radius: 2rpx;
215
+        }
216
+    }
217
+
218
+    .legend-warning {
219
+        color: #fe4322;
220
+
221
+        span {
222
+            background-color: #fe4322;
223
+        }
224
+    }
225
+
226
+    .legend-normal {
227
+        color: black;
228
+
229
+        span {
230
+            background-color: gray;
231
+        }
232
+    }
233
+
234
+    .legend-excellent {
235
+        color: #8EC742;
236
+
237
+        span {
238
+            background-color: #8EC742;
239
+        }
240
+    }
241
+
242
+    .legend-current {
243
+        color: #1890ff;
244
+
245
+        span {
246
+            background-color: #1890ff;
247
+        }
248
+    }
249
+
198 250
     .radar-list {
199 251
         display: flex;
200 252
         flex-direction: column;
@@ -208,7 +260,7 @@ export default {
208 260
             font-size: 24rpx;
209 261
 
210 262
             .item-label {
211
-                color: rgba(160, 196, 255, 0.9);
263
+                color: #333;
212 264
             }
213 265
 
214 266
             .progress-row {
@@ -219,7 +271,7 @@ export default {
219 271
                 .progress-bar {
220 272
                     flex: 1;
221 273
                     height: 10rpx;
222
-                    background: rgba(255, 255, 255, 0.1);
274
+                    background: #f0f0f0;
223 275
                     border-radius: 0;
224 276
                     overflow: visible;
225 277
 
@@ -237,6 +289,7 @@ export default {
237 289
                             width: 6rpx;
238 290
                             height: 18rpx;
239 291
                             background: #fff;
292
+                            border: 2rpx solid currentColor;
240 293
                             border-radius: 4rpx;
241 294
                         }
242 295
                     }
@@ -245,7 +298,7 @@ export default {
245 298
                 .item-value {
246 299
                     width: 80rpx;
247 300
                     text-align: right;
248
-                    color: #fff;
301
+                    color: #333;
249 302
                     font-weight: bold;
250 303
                     font-size: 24rpx;
251 304
                 }

+ 36 - 6
src/pages/components/SecurityTestCharts.vue

@@ -79,9 +79,19 @@ export default {
79 79
       const option = {
80 80
         responsive: true,
81 81
         maintainAspectRatio: false,
82
+        tooltip: {
83
+          trigger: 'item',
84
+          formatter: '{b}: {c} ({d}%)'
85
+        },
86
+        legend: {
87
+          type: 'scroll',
88
+          data: this.chartsData.items.labels,
89
+          textStyle: { color: '#666', fontSize: 10 },
90
+          top: 0
91
+        },
82 92
         series: [{
83 93
           type: 'pie',
84
-          radius: ['50%', '70%'],
94
+          radius: ['30%', '40%'],
85 95
           data: this.chartsData.items.labels.map((label, index) => ({
86 96
             name: label,
87 97
             value: this.chartsData.items.data[index]
@@ -94,7 +104,7 @@ export default {
94 104
           },
95 105
           label: {
96 106
             show: true,
97
-            color: 'rgba(255, 255, 255, 0.7)',
107
+            color: '#333',
98 108
             fontSize: 10,
99 109
             formatter: '{b}\n{d}%'
100 110
           }
@@ -108,9 +118,19 @@ export default {
108 118
       const option = {
109 119
         responsive: true,
110 120
         maintainAspectRatio: false,
121
+        legend: {
122
+          type: 'scroll',
123
+          data: this.chartsData.results.labels,
124
+          textStyle: { color: '#666', fontSize: 10 },
125
+          top: 0
126
+        },
127
+        tooltip: {
128
+          trigger: 'item',
129
+          formatter: '{b}: {c} ({d}%)'
130
+        },
111 131
         series: [{
112 132
           type: 'pie',
113
-          radius: ['50%', '70%'],
133
+          radius: ['30%', '40%'],
114 134
           data: this.chartsData.results.labels.map((label, index) => ({
115 135
             name: label,
116 136
             value: this.chartsData.results.data[index]
@@ -123,7 +143,7 @@ export default {
123 143
           },
124 144
           label: {
125 145
             show: true,
126
-            color: 'rgba(255, 255, 255, 0.7)',
146
+            color: '#333',
127 147
             fontSize: 10,
128 148
             formatter: '{b}\n{d}%'
129 149
           }
@@ -137,9 +157,19 @@ export default {
137 157
       const option = {
138 158
         responsive: true,
139 159
         maintainAspectRatio: false,
160
+        tooltip: {
161
+          trigger: 'item',
162
+          formatter: '{b}: {c} ({d}%)'
163
+        },
164
+        legend: {
165
+          type: 'scroll',
166
+          data: this.chartsData.areas.labels,
167
+          textStyle: { color: '#666', fontSize: 10 },
168
+          top: 0
169
+        },
140 170
         series: [{
141 171
           type: 'pie',
142
-          radius: ['50%', '70%'],
172
+          radius: ['30%', '40%'],
143 173
           data: this.chartsData.areas.labels.map((label, index) => ({
144 174
             name: label,
145 175
             value: this.chartsData.areas.data[index]
@@ -152,7 +182,7 @@ export default {
152 182
           },
153 183
           label: {
154 184
             show: true,
155
-            color: 'rgba(255, 255, 255, 0.7)',
185
+            color: '#333',
156 186
             fontSize: 10,
157 187
             formatter: '{b}\n{d}%'
158 188
           }

+ 11 - 8
src/pages/components/SeizedNumAll.vue

@@ -47,18 +47,23 @@ export default {
47 47
                     left: '15%',
48 48
                     right: '5%',
49 49
                     bottom: '15%',
50
-                    top: '10%'
50
+                    top: '22%'
51
+                },
52
+                legend: {
53
+                    data: ['查获数'],
54
+                    textStyle: { color: '#666', fontSize: 10 },
55
+                    top: 0
51 56
                 },
52 57
                 xAxis: {
53 58
                     type: 'category',
54 59
                     data: this.chartsData.map(item => item.recordDate),
55 60
                     axisLine: {
56 61
                         lineStyle: {
57
-                            color: 'rgba(255, 255, 255, 0.1)'
62
+                            color: 'rgba(0, 0, 0, 0.1)'
58 63
                         }
59 64
                     },
60 65
                     axisLabel: {
61
-                        color: 'rgba(255, 255, 255, 0.5)',
66
+                        color: '#666',
62 67
                         fontSize: 9,
63 68
                         rotate: 30
64 69
                     }
@@ -69,11 +74,11 @@ export default {
69 74
                     axisTick: { show: false },
70 75
                     splitLine: {
71 76
                         lineStyle: {
72
-                            color: 'rgba(255, 255, 255, 0.05)'
77
+                            color: 'rgba(0, 0, 0, 0.05)'
73 78
                         }
74 79
                     },
75 80
                     axisLabel: {
76
-                        color: 'rgba(255, 255, 255, 0.5)',
81
+                        color: '#666',
77 82
                         fontSize: 9
78 83
                     }
79 84
                 },
@@ -106,9 +111,7 @@ export default {
106 111
                 ],
107 112
                 tooltip: {
108 113
                     trigger: 'axis',
109
-                    backgroundColor: 'rgba(13,80,122,0.95)',
110
-                    borderColor: '#70CFE7',
111
-                    textStyle: { color: '#fff' }
114
+                    axisPointer: { type: 'shadow' }
112 115
                 }
113 116
             }
114 117
             this.chart.setOption(option)

+ 19 - 11
src/pages/components/SeizureInfo.vue

@@ -97,7 +97,7 @@ export default {
97 97
             top: '38%',
98 98
             style: {
99 99
               text: String(this.chartsData.total),
100
-              fill: 'rgba(255, 255, 255, 0.9)',
100
+              fill: '#333',
101 101
               fontSize: 14,
102 102
               fontWeight: 'bold',
103 103
               textAlign: 'center'
@@ -110,7 +110,7 @@ export default {
110 110
             top: '58%',
111 111
             style: {
112 112
               text: '查获总数',
113
-              fill: 'rgba(255, 255, 255, 0.5)',
113
+              fill: '#333',
114 114
               fontSize: 15,
115 115
               textAlign: 'center'
116 116
             },
@@ -126,25 +126,33 @@ export default {
126 126
       const option = {
127 127
         responsive: true,
128 128
         maintainAspectRatio: false,
129
+        tooltip: {
130
+          trigger: 'axis',
131
+          axisPointer: { type: 'shadow' }
132
+        },
133
+        legend: {
134
+          data: ['查获数量'],
135
+          textStyle: { color: '#666', fontSize: 10 },
136
+          top: 0
137
+        },
129 138
         grid: {
130 139
           left: '15%',
131 140
           right: '5%',
132 141
           bottom: '15%',
133
-          top: '10%'
142
+          top: '22%'
134 143
         },
135 144
         xAxis: {
136 145
           type: 'category',
137 146
           data: this.chartsData.depts.labels,
138 147
           axisLine: {
139 148
             lineStyle: {
140
-              color: 'rgba(255, 255, 255, 0.1)'
149
+              color: 'rgba(0, 0, 0, 0.1)'
141 150
             }
142 151
           },
143
-          // axisLabel: {
144
-          //   color: 'rgba(255, 255, 255, 0.5)',
145
-          //   fontSize: 9,
146
-          //   rotate: 30
147
-          // }
152
+          axisLabel: {
153
+            color: '#666',
154
+            fontSize: 9
155
+          }
148 156
         },
149 157
         yAxis: {
150 158
           type: 'value',
@@ -156,11 +164,11 @@ export default {
156 164
           },
157 165
           splitLine: {
158 166
             lineStyle: {
159
-              color: 'rgba(255, 255, 255, 0.05)'
167
+              color: 'rgba(0, 0, 0, 0.05)'
160 168
             }
161 169
           },
162 170
           axisLabel: {
163
-            color: 'rgba(255, 255, 255, 0.5)',
171
+            color: '#666',
164 172
             fontSize: 9
165 173
           }
166 174
         },

+ 12 - 9
src/pages/components/SupervisionDistribution.vue

@@ -47,18 +47,18 @@ export default {
47 47
                     left: '15%',
48 48
                     right: '5%',
49 49
                     bottom: '20%',
50
-                    top: '10%'
50
+                    top: '22%'
51 51
                 },
52 52
                 xAxis: {
53 53
                     type: 'category',
54 54
                     data: this.chartsData.map(item => item.name),
55 55
                     axisLine: {
56 56
                         lineStyle: {
57
-                            color: 'rgba(255, 255, 255, 0.1)'
57
+                            color: 'rgba(0, 0, 0, 0.1)'
58 58
                         }
59 59
                     },
60 60
                     axisLabel: {
61
-                        color: 'rgba(255, 255, 255, 0.5)',
61
+                        color: '#666',
62 62
                         fontSize: 9,
63 63
                         rotate: 30
64 64
                     }
@@ -67,18 +67,18 @@ export default {
67 67
                     type: 'value',
68 68
                     name: '人数',
69 69
                     nameTextStyle: {
70
-                        color: 'rgba(255, 255, 255, 0.5)',
70
+                        color: '#666',
71 71
                         fontSize: 9
72 72
                     },
73 73
                     axisLine: { show: false },
74 74
                     axisTick: { show: false },
75 75
                     splitLine: {
76 76
                         lineStyle: {
77
-                            color: 'rgba(255, 255, 255, 0.05)'
77
+                            color: 'rgba(0, 0, 0, 0.05)'
78 78
                         }
79 79
                     },
80 80
                     axisLabel: {
81
-                        color: 'rgba(255, 255, 255, 0.5)',
81
+                        color: '#666',
82 82
                         fontSize: 9
83 83
                     }
84 84
                 },
@@ -101,11 +101,14 @@ export default {
101 101
                         barWidth: '40%'
102 102
                     }
103 103
                 ],
104
+                legend: {
105
+                    data: ['查获数量'],
106
+                    textStyle: { color: '#666', fontSize: 10 },
107
+                    top: 0
108
+                },
104 109
                 tooltip: {
105 110
                     trigger: 'axis',
106
-                    backgroundColor: 'rgba(13,80,122,0.95)',
107
-                    borderColor: '#70CFE7',
108
-                    textStyle: { color: '#fff' }
111
+                    axisPointer: { type: 'shadow' }
109 112
                 }
110 113
             }
111 114
             this.chart.setOption(option)

+ 7 - 7
src/pages/components/TeamMemberTable.vue

@@ -48,25 +48,25 @@ export default {
48 48
   .table-scroll {
49 49
     width: 100%;
50 50
   }
51
-  
51
+
52 52
   .data-table {
53 53
     width: 100%;
54 54
     font-size: 24rpx;
55 55
     border-collapse: collapse;
56
-    
56
+
57 57
     th, td {
58 58
       padding: 16rpx 8rpx;
59 59
       text-align: left;
60 60
       white-space: nowrap;
61 61
     }
62
-    
62
+
63 63
     th {
64
-      color: rgba(255, 255, 255, 0.5);
65
-      border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
64
+      color: #666;
65
+      border-bottom: 1rpx solid #e0e0e0;
66 66
     }
67
-    
67
+
68 68
     td {
69
-      color: rgba(255, 255, 255, 0.9);
69
+      color: #333;
70 70
     }
71 71
   }
72 72
 }

+ 17 - 8
src/pages/components/UnsafeItemsChart.vue

@@ -57,6 +57,15 @@ export default {
57 57
       const option = {
58 58
         responsive: true,
59 59
         maintainAspectRatio: false,
60
+        tooltip: {
61
+          trigger: 'item',
62
+          formatter: '{b}: {c} ({d}%)'
63
+        },
64
+        legend: {
65
+          data: this.chartsData.items.map(item => item.name),
66
+          textStyle: { color: '#666', fontSize: 10 },
67
+          top: 0
68
+        },
60 69
         series: [{
61 70
           type: 'pie',
62 71
           radius: ['50%', '70%'],
@@ -72,7 +81,7 @@ export default {
72 81
           },
73 82
           label: {
74 83
             show: true,
75
-            color: 'rgba(255, 255, 255, 0.7)',
84
+            color: '#333',
76 85
             fontSize: 10,
77 86
             formatter: '{b}\n{d}%'
78 87
           }
@@ -90,25 +99,25 @@ export default {
90 99
 <style lang="scss" scoped>
91 100
 .unsafe-items-chart {
92 101
   .chart-container {
93
-    height: 220rpx;
102
+    height: 400rpx;
94 103
   }
95
-  
104
+
96 105
   .items-list {
97 106
     margin-top: 16rpx;
98 107
   }
99
-  
108
+
100 109
   .list-item {
101 110
     display: flex;
102 111
     justify-content: space-between;
103 112
     font-size: 24rpx;
104 113
     padding: 8rpx 0;
105
-    
114
+
106 115
     .item-name {
107
-      color: rgba(255, 255, 255, 0.7);
116
+      color: #666;
108 117
     }
109
-    
118
+
110 119
     .item-value {
111
-      color: rgba(255, 255, 255, 0.9);
120
+      color: #333;
112 121
     }
113 122
   }
114 123
 }

+ 18 - 9
src/pages/components/UnsafePositionChart.vue

@@ -52,22 +52,31 @@ export default {
52 52
       const option = {
53 53
         responsive: true,
54 54
         maintainAspectRatio: false,
55
+        tooltip: {
56
+          trigger: 'axis',
57
+          axisPointer: { type: 'shadow' }
58
+        },
59
+        legend: {
60
+          data: ['人数'],
61
+          textStyle: { color: '#666', fontSize: 10 },
62
+          top: 0
63
+        },
55 64
         grid: {
56 65
           left: '15%',
57 66
           right: '5%',
58 67
           bottom: '15%',
59
-          top: '10%'
68
+          top: '22%'
60 69
         },
61 70
         xAxis: {
62 71
           type: 'category',
63 72
           data: this.chartsData.positions.labels,
64 73
           axisLine: {
65 74
             lineStyle: {
66
-              color: 'rgba(255, 255, 255, 0.1)'
75
+              color: 'rgba(0, 0, 0, 0.1)'
67 76
             }
68 77
           },
69 78
           axisLabel: {
70
-            color: 'rgba(255, 255, 255, 0.5)',
79
+            color: '#666',
71 80
             fontSize: 10,
72 81
             // rotate: 30
73 82
           }
@@ -82,11 +91,11 @@ export default {
82 91
           },
83 92
           splitLine: {
84 93
             lineStyle: {
85
-              color: 'rgba(255, 255, 255, 0.05)'
94
+              color: 'rgba(0, 0, 0, 0.05)'
86 95
             }
87 96
           },
88 97
           axisLabel: {
89
-            color: 'rgba(255, 255, 255, 0.5)',
98
+            color: '#666',
90 99
             fontSize: 10
91 100
           }
92 101
         },
@@ -122,16 +131,16 @@ export default {
122 131
 <style lang="scss" scoped>
123 132
 .unsafe-position-chart {
124 133
   .chart-container {
125
-    height: 240rpx;
134
+    height: 400rpx;
126 135
   }
127
-  
136
+
128 137
   .position-info {
129 138
     margin-top: 16rpx;
130 139
     text-align: center;
131
-    
140
+
132 141
     .info-text {
133 142
       font-size: 24rpx;
134
-      color: rgba(255, 255, 255, 0.5);
143
+      color: #666;
135 144
     }
136 145
   }
137 146
 }

+ 17 - 8
src/pages/components/UnsafeTypesChart.vue

@@ -57,6 +57,15 @@ export default {
57 57
       const option = {
58 58
         responsive: true,
59 59
         maintainAspectRatio: false,
60
+        tooltip: {
61
+          trigger: 'item',
62
+          formatter: '{b}: {c} ({d}%)'
63
+        },
64
+        legend: {
65
+          data: this.chartsData.types.map(item => item.name),
66
+          textStyle: { color: '#666', fontSize: 10 },
67
+          top: 0
68
+        },
60 69
         series: [{
61 70
           type: 'pie',
62 71
           radius: ['50%', '70%'],
@@ -72,7 +81,7 @@ export default {
72 81
           },
73 82
           label: {
74 83
             show: true,
75
-            color: 'rgba(255, 255, 255, 0.7)',
84
+            color: '#333',
76 85
             fontSize: 10,
77 86
             formatter: '{b}\n{d}%'
78 87
           }
@@ -90,25 +99,25 @@ export default {
90 99
 <style lang="scss" scoped>
91 100
 .unsafe-types-chart {
92 101
   .chart-container {
93
-    height: 220rpx;
102
+    height: 400rpx;
94 103
   }
95
-  
104
+
96 105
   .types-list {
97 106
     margin-top: 16rpx;
98 107
   }
99
-  
108
+
100 109
   .list-item {
101 110
     display: flex;
102 111
     justify-content: space-between;
103 112
     font-size: 24rpx;
104 113
     padding: 8rpx 0;
105
-    
114
+
106 115
     .item-name {
107
-      color: rgba(255, 255, 255, 0.7);
116
+      color: #666;
108 117
     }
109
-    
118
+
110 119
     .item-value {
111
-      color: rgba(255, 255, 255, 0.9);
120
+      color: #333;
112 121
     }
113 122
   }
114 123
 }

+ 28 - 30
src/pages/deptProfile/index.vue

@@ -59,7 +59,7 @@
59 59
             </SectionTitle>
60 60
 
61 61
             <view v-if="activeTab === 'profile'">
62
-                <SectionTitle title="维得分一览">
62
+                <SectionTitle title="维得分一览">
63 63
                     <ProfileRadar :chartsData="radarData" />
64 64
                 </SectionTitle>
65 65
 
@@ -77,7 +77,7 @@
77 77
             </view>
78 78
 
79 79
             <view v-if="activeTab === 'data'">
80
-                <SectionTitle title="每日通道过检率">
80
+                <SectionTitle title="通道高峰过检率">
81 81
                     <PassengerChart :chartsData="passengerData" />
82 82
                 </SectionTitle>
83 83
 
@@ -94,7 +94,7 @@
94 94
                 </SectionTitle>
95 95
 
96 96
                 <SectionTitle title="每日查获数量">
97
-                    <DailySeizureChart :chartsData="dailySeizureData" />
97
+                    <DailySeizureChart :chartsData="dailySeizureData" :title="'班组对比'"/>
98 98
                 </SectionTitle>
99 99
 
100 100
                 <SectionTitle title="查获工作区域分布">
@@ -726,9 +726,9 @@ export default {
726 726
 </script>
727 727
 
728 728
 <style lang="scss" scoped>
729
-    .dept-selector {
729
+.dept-selector {
730 730
     padding: 16rpx 32rpx;
731
-    background: rgba(30, 27, 75, 0.8);
731
+    background: #fff;
732 732
 }
733 733
 
734 734
 .dept-select-trigger {
@@ -737,8 +737,8 @@ export default {
737 737
     justify-content: center;
738 738
     gap: 12rpx;
739 739
     padding: 16rpx 24rpx;
740
-    background: rgba(45, 42, 85, 0.8);
741
-    border: 1rpx solid rgba(167, 139, 250, 0.3);
740
+    background: #f5f5f5;
741
+    border: 1rpx solid #e0e0e0;
742 742
     border-radius: 50rpx;
743 743
 }
744 744
 
@@ -749,12 +749,12 @@ export default {
749 749
 
750 750
 .dept-name-text {
751 751
     font-size: 26rpx;
752
-    color: rgba(255, 255, 255, 0.9);
752
+    color: #333;
753 753
 }
754 754
 
755 755
 .dept-profile-page {
756 756
     min-height: 100vh;
757
-    background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
757
+    background: #fff;
758 758
     padding-bottom: 40rpx;
759 759
 }
760 760
 
@@ -762,9 +762,9 @@ export default {
762 762
     position: sticky;
763 763
     top: 0;
764 764
     z-index: 100;
765
-    background: rgba(30, 27, 75, 0.9);
765
+    background: #fff;
766 766
     backdrop-filter: blur(10px);
767
-    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
767
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
768 768
 }
769 769
 
770 770
 .header-title {
@@ -776,10 +776,7 @@ export default {
776 776
     .title-main {
777 777
         font-size: 36rpx;
778 778
         font-weight: bold;
779
-        background: linear-gradient(90deg, #A78BFA, #60A5FA);
780
-        -webkit-background-clip: text;
781
-        -webkit-text-fill-color: transparent;
782
-        background-clip: text;
779
+        color: #333;
783 780
     }
784 781
 }
785 782
 
@@ -790,13 +787,13 @@ export default {
790 787
 
791 788
     .current-time {
792 789
         font-size: 24rpx;
793
-        color: rgba(255, 255, 255, 0.6);
790
+        color: #999;
794 791
     }
795 792
 }
796 793
 
797 794
 .time-filter {
798 795
     padding: 16rpx 32rpx;
799
-    background: rgba(30, 27, 75, 0.8);
796
+    background: #fff;
800 797
 }
801 798
 
802 799
 .time-scroll {
@@ -812,14 +809,14 @@ export default {
812 809
 .time-tag {
813 810
     padding: 8rpx 10rpx;
814 811
     border-radius: 50rpx;
815
-    background: rgba(45, 42, 85, 0.8);
812
+    background: #f0f0f0;
816 813
     font-size: 24rpx;
817
-    color: rgba(255, 255, 255, 0.6);
814
+    color: #666;
818 815
     transition: all 0.3s;
819 816
 
820 817
     &.active {
821
-        background: #A78BFA;
822
-        color: #1E1B4B;
818
+        background: #60A5FA;
819
+        color: #fff;
823 820
         font-weight: 500;
824 821
     }
825 822
 }
@@ -830,26 +827,26 @@ export default {
830 827
     gap: 16rpx;
831 828
     margin-top: 16rpx;
832 829
     padding-top: 16rpx;
833
-    border-top: 1rpx solid rgba(255, 255, 255, 0.1);
830
+    border-top: 1rpx solid #e0e0e0;
834 831
 }
835 832
 
836 833
 .date-input {
837 834
     flex: 1;
838 835
     padding: 12rpx 24rpx;
839 836
     border-radius: 12rpx;
840
-    background: rgba(45, 42, 85, 0.8);
837
+    background: #f5f5f5;
841 838
     font-size: 24rpx;
842
-    color: rgba(255, 255, 255, 0.4);
839
+    color: #999;
843 840
     text-align: center;
844 841
 
845 842
     &.filled {
846
-        color: rgba(255, 255, 255, 0.9);
843
+        color: #333;
847 844
     }
848 845
 }
849 846
 
850 847
 .date-separator {
851 848
     font-size: 24rpx;
852
-    color: rgba(255, 255, 255, 0.5);
849
+    color: #999;
853 850
     flex-shrink: 0;
854 851
 }
855 852
 
@@ -862,14 +859,14 @@ export default {
862 859
 
863 860
 .tab-item {
864 861
     font-size: 28rpx;
865
-    color: rgba(255, 255, 255, 0.5);
862
+    color: #999;
866 863
     padding-bottom: 8rpx;
867 864
     border-bottom: 2rpx solid transparent;
868 865
     transition: all 0.3s;
869 866
 
870 867
     &.active {
871
-        color: #A78BFA;
872
-        border-bottom-color: #A78BFA;
868
+        color: #333;
869
+        border-bottom-color: #333;
873 870
     }
874 871
 }
875 872
 
@@ -877,5 +874,6 @@ export default {
877 874
     padding: 32rpx;
878 875
     display: flex;
879 876
     flex-direction: column;
877
+    background-color: #fff;
880 878
 }
881
-</style>
879
+</style>

Разлика између датотеке није приказан због своје велике величине
+ 546 - 172
src/pages/employeeProfile/index.vue


+ 27 - 29
src/pages/groupProfile/index.vue

@@ -77,7 +77,7 @@
77 77
             </view>
78 78
 
79 79
             <view v-if="activeTab === 'data'">
80
-                <SectionTitle title="当日开航每小时通道过检率">
80
+                <SectionTitle title="通道高峰过检率">
81 81
                     <PassengerChart :chartsData="passengerData" />
82 82
                 </SectionTitle>
83 83
 
@@ -89,12 +89,12 @@
89 89
                     <ItemDistribution :chartsData="itemDistributionData" />
90 90
                 </SectionTitle>
91 91
 
92
-                <SectionTitle title="每日查获数量(总表)">
92
+                <!-- <SectionTitle title="每日查获数量(总表)">
93 93
                     <SeizedNumAll :chartsData="dailySeizureTotalData" />
94
-                </SectionTitle>
94
+                </SectionTitle> -->
95 95
 
96 96
                 <SectionTitle title="每日查获数量">
97
-                    <DailySeizureChart :chartsData="dailySeizureData" />
97
+                    <DailySeizureChart :chartsData="dailySeizureData" :title="'个人对比'"/>
98 98
                 </SectionTitle>
99 99
 
100 100
                 <SectionTitle title="查获工作区域分布">
@@ -735,7 +735,7 @@ export default {
735 735
 <style lang="scss" scoped>
736 736
     .dept-selector {
737 737
     padding: 16rpx 32rpx;
738
-    background: rgba(30, 27, 75, 0.8);
738
+    background: #fff;
739 739
 }
740 740
 
741 741
 .dept-select-trigger {
@@ -744,8 +744,8 @@ export default {
744 744
     justify-content: center;
745 745
     gap: 12rpx;
746 746
     padding: 16rpx 24rpx;
747
-    background: rgba(45, 42, 85, 0.8);
748
-    border: 1rpx solid rgba(167, 139, 250, 0.3);
747
+    background: #f5f5f5;
748
+    border: 1rpx solid #e0e0e0;
749 749
     border-radius: 50rpx;
750 750
 }
751 751
 
@@ -756,12 +756,12 @@ export default {
756 756
 
757 757
 .dept-name-text {
758 758
     font-size: 26rpx;
759
-    color: rgba(255, 255, 255, 0.9);
759
+    color: #333;
760 760
 }
761 761
 
762 762
 .dept-profile-page {
763 763
     min-height: 100vh;
764
-    background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
764
+    background: #fff;
765 765
     padding-bottom: 40rpx;
766 766
 }
767 767
 
@@ -769,9 +769,9 @@ export default {
769 769
     position: sticky;
770 770
     top: 0;
771 771
     z-index: 100;
772
-    background: rgba(30, 27, 75, 0.9);
772
+    background: #fff;
773 773
     backdrop-filter: blur(10px);
774
-    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
774
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
775 775
 }
776 776
 
777 777
 .header-title {
@@ -783,10 +783,7 @@ export default {
783 783
     .title-main {
784 784
         font-size: 36rpx;
785 785
         font-weight: bold;
786
-        background: linear-gradient(90deg, #A78BFA, #60A5FA);
787
-        -webkit-background-clip: text;
788
-        -webkit-text-fill-color: transparent;
789
-        background-clip: text;
786
+        color: #333;
790 787
     }
791 788
 }
792 789
 
@@ -797,13 +794,13 @@ export default {
797 794
 
798 795
     .current-time {
799 796
         font-size: 24rpx;
800
-        color: rgba(255, 255, 255, 0.6);
797
+        color: #999;
801 798
     }
802 799
 }
803 800
 
804 801
 .time-filter {
805 802
     padding: 16rpx 32rpx;
806
-    background: rgba(30, 27, 75, 0.8);
803
+    background: #fff;
807 804
 }
808 805
 
809 806
 .time-scroll {
@@ -819,14 +816,14 @@ export default {
819 816
 .time-tag {
820 817
     padding: 8rpx 10rpx;
821 818
     border-radius: 50rpx;
822
-    background: rgba(45, 42, 85, 0.8);
819
+    background: #f0f0f0;
823 820
     font-size: 24rpx;
824
-    color: rgba(255, 255, 255, 0.6);
821
+    color: #666;
825 822
     transition: all 0.3s;
826 823
 
827 824
     &.active {
828
-        background: #A78BFA;
829
-        color: #1E1B4B;
825
+        background: #60A5FA;
826
+        color: #fff;
830 827
         font-weight: 500;
831 828
     }
832 829
 }
@@ -837,26 +834,26 @@ export default {
837 834
     gap: 16rpx;
838 835
     margin-top: 16rpx;
839 836
     padding-top: 16rpx;
840
-    border-top: 1rpx solid rgba(255, 255, 255, 0.1);
837
+    border-top: 1rpx solid #e0e0e0;
841 838
 }
842 839
 
843 840
 .date-input {
844 841
     flex: 1;
845 842
     padding: 12rpx 24rpx;
846 843
     border-radius: 12rpx;
847
-    background: rgba(45, 42, 85, 0.8);
844
+    background: #f5f5f5;
848 845
     font-size: 24rpx;
849
-    color: rgba(255, 255, 255, 0.4);
846
+    color: #999;
850 847
     text-align: center;
851 848
 
852 849
     &.filled {
853
-        color: rgba(255, 255, 255, 0.9);
850
+        color: #333;
854 851
     }
855 852
 }
856 853
 
857 854
 .date-separator {
858 855
     font-size: 24rpx;
859
-    color: rgba(255, 255, 255, 0.5);
856
+    color: #999;
860 857
     flex-shrink: 0;
861 858
 }
862 859
 
@@ -869,14 +866,14 @@ export default {
869 866
 
870 867
 .tab-item {
871 868
     font-size: 28rpx;
872
-    color: rgba(255, 255, 255, 0.5);
869
+    color: #999;
873 870
     padding-bottom: 8rpx;
874 871
     border-bottom: 2rpx solid transparent;
875 872
     transition: all 0.3s;
876 873
 
877 874
     &.active {
878
-        color: #A78BFA;
879
-        border-bottom-color: #A78BFA;
875
+        color: #333;
876
+        border-bottom-color: #333;
880 877
     }
881 878
 }
882 879
 
@@ -884,5 +881,6 @@ export default {
884 881
     padding: 32rpx;
885 882
     display: flex;
886 883
     flex-direction: column;
884
+    background-color: #fff;
887 885
 }
888 886
 </style>

+ 3 - 0
src/pages/home/index.vue

@@ -37,6 +37,7 @@
37 37
       </div>
38 38
     </div>
39 39
 
40
+
40 41
     <Notice ref="notice" />
41 42
     <!--  今日待办  -->
42 43
     <!-- <myToDo /> -->
@@ -118,6 +119,7 @@ export default {
118 119
 
119 120
   },
120 121
   methods: {
122
+
121 123
     goworkDocu() {
122 124
       uni.navigateTo({
123 125
         url: '/pages/workDocu/index'
@@ -367,4 +369,5 @@ img {
367 369
   width: 100%;
368 370
   margin-bottom: 32rpx;
369 371
 }
372
+
370 373
 </style>

+ 2 - 2
src/pages/login.vue

@@ -78,7 +78,7 @@ export default {
78 78
   onLoad() {
79 79
     //#ifdef H5
80 80
     if (getToken()) {
81
-      this.$tab.reLaunch('/pages/work/index')
81
+      this.$tab.reLaunch('/pages/myToDoList/index')
82 82
     }
83 83
     this.getStorage()
84 84
     //#endif
@@ -164,7 +164,7 @@ export default {
164 164
       this.$store.dispatch('GetInfo').then(res => {
165 165
         // 登录成功后调用showMessageTabRedDot更新消息tab红点状态
166 166
         showMessageTabRedDot()
167
-        this.$tab.reLaunch('/pages/work/index')
167
+        this.$tab.reLaunch('/pages/myToDoList/index')
168 168
       })
169 169
     }
170 170
   }

+ 499 - 0
src/pages/organizationStruct/index.vue

@@ -0,0 +1,499 @@
1
+<template>
2
+    <view class="organization-struct">
3
+        <!-- 顶部搜索栏 -->
4
+        <view class="search-header">
5
+            <view class="search-box">
6
+                <u-icon name="search" color="#999" size="18" />
7
+                <input 
8
+                    v-model="searchKeyword" 
9
+                    class="search-input" 
10
+                    placeholder="搜索部门或人员" 
11
+                    @input="handleSearch"
12
+                />
13
+                <u-icon v-if="searchKeyword" name="close" color="#999" size="16" @click="clearSearch" />
14
+            </view>
15
+        </view>
16
+
17
+        <!-- 面包屑导航 -->
18
+        <view v-if="!searchKeyword && (breadcrumbs.length > 0 || currentNode)" class="breadcrumb">
19
+            <view class="back-btn" v-if="breadcrumbs.length > 0 || currentNode" @click="goBack">
20
+                <u-icon name="arrow-left" color="#666" size="20" />
21
+            </view>
22
+            <view 
23
+                v-for="(item, index) in breadcrumbs" 
24
+                :key="item.id"
25
+                class="breadcrumb-item"
26
+                @click="navigateToBreadcrumb(index)"
27
+            >
28
+                <text :class="['breadcrumb-text', { active: index === breadcrumbs.length - 1 }]">
29
+                    {{ item.label }}
30
+                </text>
31
+                <u-icon v-if="index < breadcrumbs.length - 1" name="arrow-right" color="#999" size="14" />
32
+            </view>
33
+        </view>
34
+
35
+        <!-- 内容区域 -->
36
+        <scroll-view class="content-area" scroll-y>
37
+            <!-- 搜索结果 -->
38
+            <view v-if="searchKeyword" class="search-results">
39
+                <view v-if="searchResults.length === 0" class="empty-state">
40
+                    <text>暂无搜索结果</text>
41
+                </view>
42
+                <view 
43
+                    v-for="item in searchResults" 
44
+                    :key="item.id || item.userId"
45
+                    :class="['result-item', { 'is-dept': item.nodeType === 'dept' || item.children }]"
46
+                    @click="handleResultClick(item)"
47
+                >
48
+                    <view class="result-icon">
49
+                        <u-icon :name="item.nodeType === 'dept' || item.children ? 'folder' : 'account'" 
50
+                            :color="item.nodeType === 'dept' || item.children ? '#A78BFA' : '#60A5FA'" size="20" />
51
+                    </view>
52
+                    <view class="result-info">
53
+                        <text class="result-name">{{ item.label || item.nickName || item.name }}</text>
54
+                        <text v-if="item.nodeType === 'dept' || item.children" class="result-path">
55
+                            {{ getSearchResultPath(item) }}
56
+                        </text>
57
+                    </view>
58
+                </view>
59
+            </view>
60
+
61
+            <!-- 正常浏览 -->
62
+            <view v-else class="dept-list">
63
+                <!-- 部门项 -->
64
+                <view 
65
+                    v-for="item in displayDepts" 
66
+                    :key="item.id"
67
+                    class="dept-item"
68
+                    @click="navigateToDept(item)"
69
+                >
70
+                    <view class="dept-icon">
71
+                        <u-icon name="folder" color="#A78BFA" size="40" />
72
+                    </view>
73
+                    <view class="dept-info">
74
+                        <text class="dept-name">{{ item.label || item.name }}</text>
75
+                        <text class="dept-count">{{ getNodeCount(item) }}</text>
76
+                    </view>
77
+                    <u-icon name="arrow-right" color="#ccc" size="20" />
78
+                </view>
79
+
80
+                <!-- 人员项 -->
81
+                <view 
82
+                    v-for="item in displayUsers" 
83
+                    :key="item.userId"
84
+                    class="user-item"
85
+                >
86
+                    <view class="user-avatar">
87
+                        <image v-if="item.avatar" :src="item.avatar" mode="aspectFill" class="avatar-img" />
88
+                        <view v-else class="avatar-placeholder">
89
+                            <text>{{ (item.label || item.nickName || item.userName || '').slice(-1) }}</text>
90
+                        </view>
91
+                    </view>
92
+                    <view class="user-info">
93
+                        <text class="user-name">{{ item.label  }}</text>
94
+                        <text v-if="item.postNames" class="user-post">{{ item.postNames }}</text>
95
+                    </view>
96
+                </view>
97
+
98
+                <!-- 空状态 -->
99
+                <view v-if="!displayDepts.length && !displayUsers.length && !loading" class="empty-state">
100
+                    <text>暂无数据</text>
101
+                </view>
102
+
103
+                <!-- 加载中 -->
104
+                <view v-if="loading" class="loading-state">
105
+                    <u-loading-icon text="加载中" textSize="14" />
106
+                </view>
107
+            </view>
108
+        </scroll-view>
109
+    </view>
110
+</template>
111
+
112
+<script>
113
+import { getDeptUserTree } from '@/api/system/user'
114
+
115
+export default {
116
+    name: 'OrganizationStruct',
117
+    data() {
118
+        return {
119
+            loading: false,
120
+            searchKeyword: '',
121
+            treeData: [],
122
+            currentNode: null,
123
+            breadcrumbs: [],
124
+            searchResults: []
125
+        }
126
+    },
127
+    computed: {
128
+        displayDepts() {
129
+            if (!this.currentNode) {
130
+                return this.treeData.filter(item => item.nodeType === 'dept' || (item.children && item.children.length > 0))
131
+            }
132
+            return (this.currentNode.children || []).filter(item => item.nodeType === 'dept' || (item.children && item.children.length > 0))
133
+        },
134
+        displayUsers() {
135
+            if (!this.currentNode) {
136
+                return this.treeData.filter(item => item.nodeType === 'user')
137
+            }
138
+            return (this.currentNode.children || []).filter(item => item.nodeType === 'user')
139
+        }
140
+    },
141
+    mounted() {
142
+        this.fetchTreeData()
143
+    },
144
+    methods: {
145
+        async fetchTreeData() {
146
+            this.loading = true
147
+            try {
148
+                const res = await getDeptUserTree()
149
+                this.treeData = res.data || []
150
+                this.currentNode = null
151
+                this.breadcrumbs = []
152
+            } catch (error) {
153
+                console.error('获取组织架构失败:', error)
154
+                uni.showToast({ title: '获取数据失败', icon: 'none' })
155
+            } finally {
156
+                this.loading = false
157
+            }
158
+        },
159
+        getNodeCount(node) {
160
+            const children = node.children || []
161
+            const deptCount = children.filter(c => c.nodeType === 'dept' || (c.children && c.children.length > 0)).length
162
+            const userCount = children.filter(c => c.nodeType === 'user').length
163
+            const parts = []
164
+            if (deptCount > 0) parts.push(`${deptCount}个部门`)
165
+            if (userCount > 0) parts.push(`${userCount}人`)
166
+            return parts.join('、')
167
+        },
168
+        navigateToDept(node) {
169
+            this.breadcrumbs.push({
170
+                id: node.id,
171
+                label: node.label || node.name
172
+            })
173
+            this.currentNode = node
174
+        },
175
+        goBack() {
176
+            if (this.breadcrumbs.length <= 1) {
177
+                this.currentNode = null
178
+                this.breadcrumbs = []
179
+            } else {
180
+                this.breadcrumbs.pop()
181
+                if (this.breadcrumbs.length === 0) {
182
+                    this.currentNode = null
183
+                } else {
184
+                    const targetId = this.breadcrumbs[this.breadcrumbs.length - 1].id
185
+                    this.currentNode = this.findNodeById(this.treeData, targetId)
186
+                }
187
+            }
188
+        },
189
+        navigateToBreadcrumb(index) {
190
+            if (index === 0) {
191
+                this.currentNode = null
192
+                this.breadcrumbs = []
193
+            } else {
194
+                const targetId = this.breadcrumbs[index].id
195
+                let targetNode = this.findNodeById(this.treeData, targetId)
196
+                if (targetNode) {
197
+                    this.currentNode = targetNode
198
+                    this.breadcrumbs = this.breadcrumbs.slice(0, index + 1)
199
+                }
200
+            }
201
+        },
202
+        findNodeById(nodes, id) {
203
+            for (const node of nodes) {
204
+                if (node.id === id) return node
205
+                if (node.children && node.children.length > 0) {
206
+                    const found = this.findNodeById(node.children, id)
207
+                    if (found) return found
208
+                }
209
+            }
210
+            return null
211
+        },
212
+        handleSearch() {
213
+            if (!this.searchKeyword.trim()) {
214
+                this.searchResults = []
215
+                return
216
+            }
217
+            const keyword = this.searchKeyword.trim().toLowerCase()
218
+            this.searchResults = this.searchTree(this.treeData, keyword)
219
+        },
220
+        searchTree(nodes, keyword, path = []) {
221
+            let results = []
222
+            for (const node of nodes) {
223
+                const name = (node.label || node.nickName || node.name || '').toLowerCase()
224
+                if (name.includes(keyword)) {
225
+                    results.push({
226
+                        ...node,
227
+                        searchPath: [...path]
228
+                    })
229
+                }
230
+                if (node.children && node.children.length > 0) {
231
+                    results = results.concat(
232
+                        this.searchTree(node.children, keyword, [...path, node.label || node.name])
233
+                    )
234
+                }
235
+            }
236
+            return results
237
+        },
238
+        getSearchResultPath(item) {
239
+            if (!item.searchPath || !item.searchPath.length) return ''
240
+            return item.searchPath.join(' > ')
241
+        },
242
+        handleResultClick(item) {
243
+            if (item.children || item.nodeType === 'dept') {
244
+                this.searchKeyword = ''
245
+                this.searchResults = []
246
+                const targetNode = this.findNodeById(this.treeData, item.id)
247
+                if (targetNode) {
248
+                    this.navigateToNodeFromRoot(targetNode)
249
+                }
250
+            }
251
+        },
252
+        navigateToNodeFromRoot(targetNode) {
253
+            const path = this.findPathToNode(this.treeData, targetNode.id)
254
+            if (path.length > 0) {
255
+                this.breadcrumbs = path.slice(0, -1).map(node => ({
256
+                    id: node.id,
257
+                    label: node.label || node.name
258
+                }))
259
+                this.currentNode = targetNode
260
+            }
261
+        },
262
+        findPathToNode(nodes, id, path = []) {
263
+            for (const node of nodes) {
264
+                const newPath = [...path, node]
265
+                if (node.id === id) {
266
+                    return newPath
267
+                }
268
+                if (node.children && node.children.length > 0) {
269
+                    const found = this.findPathToNode(node.children, id, newPath)
270
+                    if (found.length > 0) {
271
+                        return found
272
+                    }
273
+                }
274
+            }
275
+            return []
276
+        },
277
+        clearSearch() {
278
+            this.searchKeyword = ''
279
+            this.searchResults = []
280
+        }
281
+    }
282
+}
283
+</script>
284
+
285
+<style lang="scss" scoped>
286
+.organization-struct {
287
+    min-height: 100vh;
288
+    background-color: #f5f7fa;
289
+    display: flex;
290
+    flex-direction: column;
291
+}
292
+
293
+.search-header {
294
+    padding: 24rpx 32rpx;
295
+    background-color: #fff;
296
+}
297
+
298
+.search-box {
299
+    display: flex;
300
+    align-items: center;
301
+    gap: 16rpx;
302
+    padding: 16rpx 24rpx;
303
+    background-color: #f5f7fa;
304
+    border-radius: 48rpx;
305
+}
306
+
307
+.search-input {
308
+    flex: 1;
309
+    font-size: 28rpx;
310
+    color: #333;
311
+}
312
+
313
+.breadcrumb {
314
+    display: flex;
315
+    align-items: center;
316
+    padding: 20rpx 32rpx;
317
+    background-color: #fff;
318
+    overflow-x: auto;
319
+    white-space: nowrap;
320
+    border-bottom: 1rpx solid #eee;
321
+}
322
+
323
+.back-btn {
324
+    display: flex;
325
+    align-items: center;
326
+    justify-content: center;
327
+    padding-right: 16rpx;
328
+    flex-shrink: 0;
329
+}
330
+
331
+.breadcrumb-item {
332
+    display: flex;
333
+    align-items: center;
334
+    gap: 8rpx;
335
+    flex-shrink: 0;
336
+}
337
+
338
+.breadcrumb-text {
339
+    font-size: 28rpx;
340
+    color: #999;
341
+
342
+    &.active {
343
+        color: #333;
344
+        font-weight: 500;
345
+    }
346
+}
347
+
348
+.content-area {
349
+    flex: 1;
350
+    padding: 24rpx 32rpx;
351
+}
352
+
353
+.search-results {
354
+    background-color: #fff;
355
+    border-radius: 16rpx;
356
+    padding: 16rpx 0;
357
+}
358
+
359
+.result-item {
360
+    display: flex;
361
+    align-items: center;
362
+    padding: 24rpx 32rpx;
363
+    gap: 20rpx;
364
+    border-bottom: 1rpx solid #f5f5f5;
365
+}
366
+
367
+.result-icon {
368
+    width: 64rpx;
369
+    height: 64rpx;
370
+    display: flex;
371
+    align-items: center;
372
+    justify-content: center;
373
+}
374
+
375
+.result-info {
376
+    flex: 1;
377
+    display: flex;
378
+    flex-direction: column;
379
+    gap: 8rpx;
380
+}
381
+
382
+.result-name {
383
+    font-size: 30rpx;
384
+    color: #333;
385
+}
386
+
387
+.result-path {
388
+    font-size: 24rpx;
389
+    color: #999;
390
+}
391
+
392
+.dept-list {
393
+    display: flex;
394
+    flex-direction: column;
395
+    gap: 20rpx;
396
+}
397
+
398
+.dept-item {
399
+    display: flex;
400
+    align-items: center;
401
+    padding: 24rpx 32rpx;
402
+    background-color: #fff;
403
+    border-radius: 16rpx;
404
+    gap: 20rpx;
405
+}
406
+
407
+.dept-icon {
408
+    width: 80rpx;
409
+    height: 80rpx;
410
+    display: flex;
411
+    align-items: center;
412
+    justify-content: center;
413
+    background-color: #f5f5f5;
414
+    border-radius: 12rpx;
415
+}
416
+
417
+.dept-info {
418
+    flex: 1;
419
+    display: flex;
420
+    flex-direction: column;
421
+    gap: 8rpx;
422
+}
423
+
424
+.dept-name {
425
+    font-size: 32rpx;
426
+    color: #333;
427
+    font-weight: 500;
428
+}
429
+
430
+.dept-count {
431
+    font-size: 24rpx;
432
+    color: #999;
433
+}
434
+
435
+.user-item {
436
+    display: flex;
437
+    align-items: center;
438
+    padding: 24rpx 32rpx;
439
+    background-color: #fff;
440
+    border-radius: 16rpx;
441
+    gap: 20rpx;
442
+}
443
+
444
+.user-avatar {
445
+    width: 80rpx;
446
+    height: 80rpx;
447
+    flex-shrink: 0;
448
+}
449
+
450
+.avatar-img {
451
+    width: 100%;
452
+    height: 100%;
453
+    border-radius: 50%;
454
+}
455
+
456
+.avatar-placeholder {
457
+    width: 100%;
458
+    height: 100%;
459
+    border-radius: 50%;
460
+    background: linear-gradient(135deg, #60A5FA, #A78BFA);
461
+    display: flex;
462
+    align-items: center;
463
+    justify-content: center;
464
+
465
+    text {
466
+        font-size: 32rpx;
467
+        color: #fff;
468
+        font-weight: 500;
469
+    }
470
+}
471
+
472
+.user-info {
473
+    flex: 1;
474
+    display: flex;
475
+    flex-direction: column;
476
+    gap: 6rpx;
477
+}
478
+
479
+.user-name {
480
+    font-size: 32rpx;
481
+    color: #333;
482
+    font-weight: 500;
483
+}
484
+
485
+.user-post {
486
+    font-size: 24rpx;
487
+    color: #999;
488
+}
489
+
490
+.empty-state,
491
+.loading-state {
492
+    padding: 80rpx 32rpx;
493
+    display: flex;
494
+    align-items: center;
495
+    justify-content: center;
496
+    color: #999;
497
+    font-size: 28rpx;
498
+}
499
+</style>

+ 0 - 0
src/pages/organizationStruct/组织架构


+ 105 - 0
src/pages/profileManage/index.vue

@@ -0,0 +1,105 @@
1
+<template>
2
+  <home-container :customStyle="{ background: 'none', backgroundColor: '#f5f5f5' }">
3
+    <view class="profile-manage-container">
4
+      <view class="grid-body">
5
+        <uni-section title="画像管理" type="line" class="uni-section-custom"></uni-section>
6
+        <uni-grid :column="3" :showBorder="false">
7
+          <uni-grid-item v-for="(item, index) in items" :key="index">
8
+            <view class="grid-item-box" @click="handleGridClick(item.appUrl)">
9
+              <img alt="" :src="item.workbenchIcon">
10
+              <text class="text">{{ item.appName }}</text>
11
+            </view>
12
+          </uni-grid-item>
13
+        </uni-grid>
14
+      </view>
15
+    </view>
16
+  </home-container>
17
+</template>
18
+
19
+<script>
20
+import HomeContainer from '@/components/HomeContainer.vue'
21
+import { getAppList } from '@/api/system/user'
22
+
23
+const profileAppNames = ['站画像', '部门画像', '班组画像', '小组画像', '员工画像']
24
+
25
+export default {
26
+  components: { HomeContainer },
27
+  data() {
28
+    return {
29
+      appList: []
30
+    }
31
+  },
32
+  computed: {
33
+    items() {
34
+      return this.appList.filter(item => profileAppNames.includes(item.appName))
35
+    }
36
+  },
37
+  onLoad() {
38
+    this.loadAppList()
39
+  },
40
+  methods: {
41
+    async loadAppList() {
42
+      try {
43
+        const res = await getAppList({ homePage: 0 })
44
+        if (res.code === 200) {
45
+          this.appList = res.data || []
46
+        }
47
+      } catch (error) {
48
+        console.error('获取画像应用列表失败:', error)
49
+        this.appList = []
50
+      }
51
+    },
52
+    handleGridClick(url) {
53
+      if (!url) return
54
+      uni.navigateTo({ url })
55
+    }
56
+  }
57
+}
58
+</script>
59
+
60
+<style lang="scss" scoped>
61
+.profile-manage-container {
62
+  padding: 20rpx;
63
+  background-color: #f5f5f5;
64
+}
65
+
66
+.grid-body {
67
+  background-color: #fff;
68
+  border-radius: 16rpx;
69
+  padding: 20rpx;
70
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
71
+
72
+  .uni-section-custom {
73
+    ::v-deep .uni-section-header {
74
+      padding: 12rpx 10rpx !important;
75
+    }
76
+  }
77
+}
78
+
79
+.grid-item-box {
80
+  flex: 1;
81
+  display: flex;
82
+  flex-direction: column;
83
+  align-items: center;
84
+  justify-content: center;
85
+  padding: 30rpx 0;
86
+
87
+  img {
88
+    display: inline-block;
89
+    width: 88rpx;
90
+    height: 88rpx;
91
+  }
92
+
93
+  &:active {
94
+    background-color: #f5f5f5;
95
+    border-radius: 12rpx;
96
+  }
97
+}
98
+
99
+.text {
100
+  text-align: center;
101
+  font-size: 25rpx;
102
+  margin-top: 15rpx;
103
+  color: #333;
104
+}
105
+</style>

+ 20 - 27
src/pages/stationProfile/index.vue

@@ -588,7 +588,7 @@ export default {
588 588
 <style lang="scss" scoped>
589 589
 .station-profile-page {
590 590
     min-height: 100vh;
591
-    background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
591
+    background: #fff;
592 592
     padding-bottom: 40rpx;
593 593
 }
594 594
 
@@ -596,9 +596,9 @@ export default {
596 596
     position: sticky;
597 597
     top: 0;
598 598
     z-index: 100;
599
-    background: rgba(30, 27, 75, 0.9);
599
+    background: #fff;
600 600
     backdrop-filter: blur(10px);
601
-    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
601
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
602 602
 }
603 603
 
604 604
 .header-title {
@@ -610,10 +610,7 @@ export default {
610 610
     .title-main {
611 611
         font-size: 36rpx;
612 612
         font-weight: bold;
613
-        background: linear-gradient(90deg, #A78BFA, #60A5FA);
614
-        -webkit-background-clip: text;
615
-        -webkit-text-fill-color: transparent;
616
-        background-clip: text;
613
+        color: #333;
617 614
     }
618 615
 }
619 616
 
@@ -624,18 +621,13 @@ export default {
624 621
 
625 622
     .current-time {
626 623
         font-size: 24rpx;
627
-        color: rgba(255, 255, 255, 0.6);
628
-    }
629
-
630
-    .menu-icon {
631
-        display: flex;
632
-        align-items: center;
624
+        color: #999;
633 625
     }
634 626
 }
635 627
 
636 628
 .time-filter {
637 629
     padding: 16rpx 32rpx;
638
-    background: rgba(30, 27, 75, 0.8);
630
+    background: #fff;
639 631
 }
640 632
 
641 633
 .time-scroll {
@@ -651,14 +643,14 @@ export default {
651 643
 .time-tag {
652 644
     padding: 8rpx 10rpx;
653 645
     border-radius: 50rpx;
654
-    background: rgba(45, 42, 85, 0.8);
646
+    background: #f0f0f0;
655 647
     font-size: 24rpx;
656
-    color: rgba(255, 255, 255, 0.6);
648
+    color: #666;
657 649
     transition: all 0.3s;
658 650
 
659 651
     &.active {
660
-        background: #A78BFA;
661
-        color: #1E1B4B;
652
+        background: #60A5FA;
653
+        color: #fff;
662 654
         font-weight: 500;
663 655
     }
664 656
 }
@@ -669,26 +661,26 @@ export default {
669 661
     gap: 16rpx;
670 662
     margin-top: 16rpx;
671 663
     padding-top: 16rpx;
672
-    border-top: 1rpx solid rgba(255, 255, 255, 0.1);
664
+    border-top: 1rpx solid #e0e0e0;
673 665
 }
674 666
 
675 667
 .date-input {
676 668
     flex: 1;
677 669
     padding: 12rpx 24rpx;
678 670
     border-radius: 12rpx;
679
-    background: rgba(45, 42, 85, 0.8);
671
+    background: #f5f5f5;
680 672
     font-size: 24rpx;
681
-    color: rgba(255, 255, 255, 0.4);
673
+    color: #999;
682 674
     text-align: center;
683 675
 
684 676
     &.filled {
685
-        color: rgba(255, 255, 255, 0.9);
677
+        color: #333;
686 678
     }
687 679
 }
688 680
 
689 681
 .date-separator {
690 682
     font-size: 24rpx;
691
-    color: rgba(255, 255, 255, 0.5);
683
+    color: #999;
692 684
     flex-shrink: 0;
693 685
 }
694 686
 
@@ -701,14 +693,14 @@ export default {
701 693
 
702 694
 .tab-item {
703 695
     font-size: 28rpx;
704
-    color: rgba(255, 255, 255, 0.5);
696
+    color: #999;
705 697
     padding-bottom: 8rpx;
706 698
     border-bottom: 2rpx solid transparent;
707 699
     transition: all 0.3s;
708 700
 
709 701
     &.active {
710
-        color: #A78BFA;
711
-        border-bottom-color: #A78BFA;
702
+        color: #333;
703
+        border-bottom-color: #333;
712 704
     }
713 705
 }
714 706
 
@@ -716,9 +708,10 @@ export default {
716 708
     padding: 32rpx;
717 709
     display: flex;
718 710
     flex-direction: column;
719
-    /* gap: 32rpx; */
711
+    background-color: #fff;
720 712
 }
721 713
 
714
+
722 715
 .two-col-grid {
723 716
     display: grid;
724 717
     grid-template-columns: repeat(2, 1fr);

+ 24 - 27
src/pages/teamProfile/index.vue

@@ -77,7 +77,7 @@
77 77
             </view>
78 78
 
79 79
             <view v-if="activeTab === 'data'">
80
-                <SectionTitle title="当日开航每小时通道过检率">
80
+                <SectionTitle title="通道高峰过检率">
81 81
                     <PassengerChart :chartsData="passengerData" />
82 82
                 </SectionTitle>
83 83
 
@@ -94,7 +94,7 @@
94 94
                 </SectionTitle>
95 95
 
96 96
                 <SectionTitle title="每日查获数量">
97
-                    <DailySeizureChart :chartsData="dailySeizureData" />
97
+                    <DailySeizureChart :chartsData="dailySeizureData" :title="'小组对比'"/>
98 98
                 </SectionTitle>
99 99
 
100 100
                 <SectionTitle title="查获工作区域分布">
@@ -751,7 +751,7 @@ export default {
751 751
 <style lang="scss" scoped>
752 752
 .dept-selector {
753 753
     padding: 16rpx 32rpx;
754
-    background: rgba(30, 27, 75, 0.8);
754
+    background: #fff;
755 755
 }
756 756
 
757 757
 .dept-select-trigger {
@@ -760,8 +760,8 @@ export default {
760 760
     justify-content: center;
761 761
     gap: 12rpx;
762 762
     padding: 16rpx 24rpx;
763
-    background: rgba(45, 42, 85, 0.8);
764
-    border: 1rpx solid rgba(167, 139, 250, 0.3);
763
+    background: #f5f5f5;
764
+    border: 1rpx solid #e0e0e0;
765 765
     border-radius: 50rpx;
766 766
 }
767 767
 
@@ -772,12 +772,12 @@ export default {
772 772
 
773 773
 .dept-name-text {
774 774
     font-size: 26rpx;
775
-    color: rgba(255, 255, 255, 0.9);
775
+    color: #333;
776 776
 }
777 777
 
778 778
 .dept-profile-page {
779 779
     min-height: 100vh;
780
-    background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
780
+    background: #fff;
781 781
     padding-bottom: 40rpx;
782 782
 }
783 783
 
@@ -785,9 +785,9 @@ export default {
785 785
     position: sticky;
786 786
     top: 0;
787 787
     z-index: 100;
788
-    background: rgba(30, 27, 75, 0.9);
788
+    background: #fff;
789 789
     backdrop-filter: blur(10px);
790
-    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
790
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
791 791
 }
792 792
 
793 793
 .header-title {
@@ -799,10 +799,7 @@ export default {
799 799
     .title-main {
800 800
         font-size: 36rpx;
801 801
         font-weight: bold;
802
-        background: linear-gradient(90deg, #A78BFA, #60A5FA);
803
-        -webkit-background-clip: text;
804
-        -webkit-text-fill-color: transparent;
805
-        background-clip: text;
802
+        color: #333;
806 803
     }
807 804
 }
808 805
 
@@ -813,13 +810,13 @@ export default {
813 810
 
814 811
     .current-time {
815 812
         font-size: 24rpx;
816
-        color: rgba(255, 255, 255, 0.6);
813
+        color: #999;
817 814
     }
818 815
 }
819 816
 
820 817
 .time-filter {
821 818
     padding: 16rpx 32rpx;
822
-    background: rgba(30, 27, 75, 0.8);
819
+    background: #fff;
823 820
 }
824 821
 
825 822
 .time-scroll {
@@ -835,14 +832,14 @@ export default {
835 832
 .time-tag {
836 833
     padding: 8rpx 10rpx;
837 834
     border-radius: 50rpx;
838
-    background: rgba(45, 42, 85, 0.8);
835
+    background: #f0f0f0;
839 836
     font-size: 24rpx;
840
-    color: rgba(255, 255, 255, 0.6);
837
+    color: #666;
841 838
     transition: all 0.3s;
842 839
 
843 840
     &.active {
844
-        background: #A78BFA;
845
-        color: #1E1B4B;
841
+        background: #60A5FA;
842
+        color: #fff;
846 843
         font-weight: 500;
847 844
     }
848 845
 }
@@ -853,26 +850,26 @@ export default {
853 850
     gap: 16rpx;
854 851
     margin-top: 16rpx;
855 852
     padding-top: 16rpx;
856
-    border-top: 1rpx solid rgba(255, 255, 255, 0.1);
853
+    border-top: 1rpx solid #e0e0e0;
857 854
 }
858 855
 
859 856
 .date-input {
860 857
     flex: 1;
861 858
     padding: 12rpx 24rpx;
862 859
     border-radius: 12rpx;
863
-    background: rgba(45, 42, 85, 0.8);
860
+    background: #f5f5f5;
864 861
     font-size: 24rpx;
865
-    color: rgba(255, 255, 255, 0.4);
862
+    color: #999;
866 863
     text-align: center;
867 864
 
868 865
     &.filled {
869
-        color: rgba(255, 255, 255, 0.9);
866
+        color: #333;
870 867
     }
871 868
 }
872 869
 
873 870
 .date-separator {
874 871
     font-size: 24rpx;
875
-    color: rgba(255, 255, 255, 0.5);
872
+    color: #999;
876 873
     flex-shrink: 0;
877 874
 }
878 875
 
@@ -885,14 +882,14 @@ export default {
885 882
 
886 883
 .tab-item {
887 884
     font-size: 28rpx;
888
-    color: rgba(255, 255, 255, 0.5);
885
+    color: #999;
889 886
     padding-bottom: 8rpx;
890 887
     border-bottom: 2rpx solid transparent;
891 888
     transition: all 0.3s;
892 889
 
893 890
     &.active {
894
-        color: #A78BFA;
895
-        border-bottom-color: #A78BFA;
891
+        color: #333;
892
+        border-bottom-color: #333;
896 893
     }
897 894
 }
898 895
 

+ 7 - 2
src/pages/work/index.vue

@@ -33,13 +33,15 @@
33 33
         </uni-grid>
34 34
       </view>
35 35
     </div>
36
+
37
+   
36 38
   </home-container>
37 39
 </template>
38 40
 <script>
39 41
 import HomeContainer from "@/components/HomeContainer.vue";
40 42
 import { checkRolePermission } from "@/utils/common.js";
41 43
 import { getUserProfile, getAppListByRoleId,getAppList } from "@/api/system/user";
42
-
44
+const profileAppNames = ['站画像', '部门画像', '班组画像', '小组画像', '员工画像']
43 45
 export default {
44 46
   components: { HomeContainer },
45 47
   data() {
@@ -53,7 +55,7 @@ export default {
53 55
       return this.$store?.state?.user?.roles[0]
54 56
     },
55 57
     items() {
56
-      return this.appList
58
+      return this.appList.filter(item => !profileAppNames.includes(item.appName))
57 59
     }
58 60
   },
59 61
   onShow() {
@@ -93,6 +95,7 @@ export default {
93 95
     updateCurrentDate() {
94 96
       this.currentDate = this.formatDate(new Date());
95 97
     },
98
+   
96 99
     handleGridClick(url) {
97 100
       console.log('点击了宫格:', url, this.role);
98 101
       uni.navigateTo({
@@ -218,4 +221,6 @@ export default {
218 221
   font-size: 24rpx;
219 222
   color: #999;
220 223
 }
224
+
225
+
221 226
 </style>

BIN
src/static/images/tabbar/ai.png


BIN
src/static/images/tabbar/ai_.png