Sfoglia il codice sorgente

feat: 新增班组和小组详情页面,优化部门选择组件与雷达图容器

1.  新增teamProfile和groupProfile页面,实现班组/小组综合信息展示
2.  优化DeptSelector组件:支持自定义标题、修复弹窗遮罩逻辑、优化滚动与布局
3.  替换ProfileRadar和员工页面的view标签为div适配echarts
4.  优化雷达图指标最大值计算逻辑,移除调试代码
huoyi 2 settimane fa
parent
commit
eda0f26a61

+ 13 - 5
src/pages/components/DeptSelector.vue

@@ -1,15 +1,15 @@
1
 <template>
1
 <template>
2
     <u-popup :show="show" mode="bottom" :round="16" :mask-close-able="true" :safe-area-inset-bottom="true"
2
     <u-popup :show="show" mode="bottom" :round="16" :mask-close-able="true" :safe-area-inset-bottom="true"
3
-        @close="onClose">
3
+        :close-on-click-mask="true" @close="onClose">
4
         <view class="dept-picker">
4
         <view class="dept-picker">
5
             <view class="picker-header">
5
             <view class="picker-header">
6
-                <text class="picker-title">选择部门</text>
6
+                <text class="picker-title">选择{{ title }}</text>
7
             </view>
7
             </view>
8
             <view class="search-box">
8
             <view class="search-box">
9
-                <u-input v-model="deptSearchKeyword" placeholder="搜索部门" @confirm="onDeptSearch"
9
+                <u-input v-model="deptSearchKeyword" :placeholder="`搜索${title}`" :border="false" @confirm="onDeptSearch"
10
                     @input="onDeptSearch" />
10
                     @input="onDeptSearch" />
11
             </view>
11
             </view>
12
-            <scroll-view scroll-y class="dept-list">
12
+            <scroll-view scroll-y class="dept-list" :enable-back-to-top="true">
13
                 <view class="dept-item" v-for="item in filteredDeptList" :key="item.deptId"
13
                 <view class="dept-item" v-for="item in filteredDeptList" :key="item.deptId"
14
                     @click="onDeptSelect(item)">
14
                     @click="onDeptSelect(item)">
15
                     <text class="dept-item-name">{{ item.deptName }}</text>
15
                     <text class="dept-item-name">{{ item.deptName }}</text>
@@ -31,6 +31,10 @@
31
 export default {
31
 export default {
32
     name: 'DeptSelector',
32
     name: 'DeptSelector',
33
     props: {
33
     props: {
34
+        title: {
35
+            type: String,
36
+            default: '部门'
37
+        },
34
         show: {
38
         show: {
35
             type: Boolean,
39
             type: Boolean,
36
             default: false
40
             default: false
@@ -100,7 +104,7 @@ export default {
100
     flex-direction: column;
104
     flex-direction: column;
101
     border-radius: 16rpx 16rpx 0 0;
105
     border-radius: 16rpx 16rpx 0 0;
102
     width: 100%;
106
     width: 100%;
103
-    max-height: 70vh;
107
+    height: 70vh;
104
 }
108
 }
105
 
109
 
106
 .picker-header {
110
 .picker-header {
@@ -108,6 +112,7 @@ export default {
108
     border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
112
     border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
109
     display: flex;
113
     display: flex;
110
     justify-content: center;
114
     justify-content: center;
115
+    flex-shrink: 0;
111
 }
116
 }
112
 
117
 
113
 .picker-title {
118
 .picker-title {
@@ -118,11 +123,14 @@ export default {
118
 
123
 
119
 .search-box {
124
 .search-box {
120
     padding: 16rpx 32rpx;
125
     padding: 16rpx 32rpx;
126
+    flex-shrink: 0;
121
 }
127
 }
122
 
128
 
123
 .dept-list {
129
 .dept-list {
124
     flex: 1;
130
     flex: 1;
125
     padding: 0 32rpx;
131
     padding: 0 32rpx;
132
+    height: 0;
133
+    overflow: hidden;
126
 }
134
 }
127
 
135
 
128
 .dept-item {
136
 .dept-item {

+ 1 - 1
src/pages/components/ProfileRadar.vue

@@ -4,7 +4,7 @@
4
             <text>暂无数据</text>
4
             <text>暂无数据</text>
5
         </view>
5
         </view>
6
         <template v-else>
6
         <template v-else>
7
-            <view class="chart-container" ref="radarChart"></view>
7
+            <div class="chart-container" ref="radarChart"></div>
8
             <view class="radar-list">
8
             <view class="radar-list">
9
                 <view v-for="(item, index) in radarData" :key="index" class="radar-item">
9
                 <view v-for="(item, index) in radarData" :key="index" class="radar-item">
10
                     <text class="item-label">{{ item.name }}</text>
10
                     <text class="item-label">{{ item.name }}</text>

+ 9 - 6
src/pages/employeeProfile/index.vue

@@ -104,7 +104,7 @@
104
                         </view>
104
                         </view>
105
 
105
 
106
                         <view class="score-section">
106
                         <view class="score-section">
107
-                            <view class="score-circle" ref="scoreCircle"></view>
107
+                            <div class="score-circle" ref="scoreCircle"></div>
108
                             <view class="score-box">
108
                             <view class="score-box">
109
                                 <view class="score-row">
109
                                 <view class="score-row">
110
                                     <text class="score-label">评分:</text>
110
                                     <text class="score-label">评分:</text>
@@ -146,7 +146,7 @@
146
                 </SectionTitle>
146
                 </SectionTitle>
147
 
147
 
148
                 <SectionTitle v-if="activeTab === 'profile'" title="个人能力">
148
                 <SectionTitle v-if="activeTab === 'profile'" title="个人能力">
149
-                    <view class="chart-container" ref="radarChart"></view>
149
+                    <div class="chart-container" ref="radarChart"></div>
150
                 </SectionTitle>
150
                 </SectionTitle>
151
 
151
 
152
                 <SectionTitle v-if="activeTab === 'profile'" title="补充信息">
152
                 <SectionTitle v-if="activeTab === 'profile'" title="补充信息">
@@ -548,7 +548,7 @@ export default {
548
             }).catch(() => { })
548
             }).catch(() => { })
549
 
549
 
550
             const tagPromise = countTagScore(params).then(res => {
550
             const tagPromise = countTagScore(params).then(res => {
551
-                debugger
551
+                
552
                 const data = res.data
552
                 const data = res.data
553
                 if (Array.isArray(data)) {
553
                 if (Array.isArray(data)) {
554
                     const found = data.find(item => item.userId === this.selectedEmployeeId)
554
                     const found = data.find(item => item.userId === this.selectedEmployeeId)
@@ -560,20 +560,21 @@ export default {
560
 
560
 
561
             Promise.all([portraitPromise, tagPromise]).finally(() => {
561
             Promise.all([portraitPromise, tagPromise]).finally(() => {
562
                 this.$nextTick(() => {
562
                 this.$nextTick(() => {
563
+                    
563
                     this.initRadarChart()
564
                     this.initRadarChart()
564
                     this.initScoreChart()
565
                     this.initScoreChart()
565
                 })
566
                 })
566
             })
567
             })
567
         },
568
         },
568
         initScoreChart() {
569
         initScoreChart() {
569
-            debugger
570
+            
570
             if (!this.$refs.scoreCircle) return
571
             if (!this.$refs.scoreCircle) return
571
             if (this.scoreChartInstance) {
572
             if (this.scoreChartInstance) {
572
                 this.scoreChartInstance.dispose()
573
                 this.scoreChartInstance.dispose()
573
             }
574
             }
574
             this.scoreChartInstance = echarts.init(this.$refs.scoreCircle)
575
             this.scoreChartInstance = echarts.init(this.$refs.scoreCircle)
575
             const score = this.portrait.totalScore || 0
576
             const score = this.portrait.totalScore || 0
576
-            debugger
577
+            
577
             const option = {
578
             const option = {
578
                 series: [
579
                 series: [
579
                     {
580
                     {
@@ -643,15 +644,17 @@ export default {
643
             this.scoreChartInstance.setOption(option)
644
             this.scoreChartInstance.setOption(option)
644
         },
645
         },
645
         initRadarChart() {
646
         initRadarChart() {
647
+            
646
             if (!this.$refs.radarChart) return
648
             if (!this.$refs.radarChart) return
647
             if (this.radarChartInstance) {
649
             if (this.radarChartInstance) {
648
                 this.radarChartInstance.dispose()
650
                 this.radarChartInstance.dispose()
649
             }
651
             }
650
             this.radarChartInstance = echarts.init(this.$refs.radarChart)
652
             this.radarChartInstance = echarts.init(this.$refs.radarChart)
651
             const dimensions = this.portrait.dimensions || []
653
             const dimensions = this.portrait.dimensions || []
654
+            const maxScore = dimensions.length > 0 ? Math.max(...dimensions.map(d => d.score || 0)) : 100
652
             const indicator = dimensions.map(d => ({
655
             const indicator = dimensions.map(d => ({
653
                 name: d.name + '\n' + d.score,
656
                 name: d.name + '\n' + d.score,
654
-                max: 100
657
+                max: maxScore
655
             }))
658
             }))
656
             const option = {
659
             const option = {
657
                 radar: {
660
                 radar: {

+ 895 - 0
src/pages/groupProfile/index.vue

@@ -0,0 +1,895 @@
1
+<template>
2
+    <view class="dept-profile-page">
3
+        <view class="page-header">
4
+            <view class="header-title">
5
+                <view class="title-main">小组综合信息展示</view>
6
+                <view class="header-right">
7
+                    <view class="current-time">{{ currentTime }}</view>
8
+                </view>
9
+            </view>
10
+
11
+            <view class="time-filter">
12
+                <scroll-view scroll-x class="time-scroll">
13
+                    <view class="time-tags">
14
+                        <view v-for="(tag, index) in timeTags" :key="index"
15
+                            :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
16
+                            {{ tag }}
17
+                        </view>
18
+                    </view>
19
+                </scroll-view>
20
+                <view v-if="selectedTimeTag === 4" class="date-range-picker">
21
+                    <picker mode="date" :value="startDate" @change="onStartDateChange">
22
+                        <view class="date-input" :class="{ filled: startDate }">
23
+                            {{ startDate || '开始日期' }}
24
+                        </view>
25
+                    </picker>
26
+                    <text class="date-separator">至</text>
27
+                    <picker mode="date" :value="endDate" @change="onEndDateChange">
28
+                        <view class="date-input" :class="{ filled: endDate }">
29
+                            {{ endDate || '结束日期' }}
30
+                        </view>
31
+                    </picker>
32
+                </view>
33
+            </view>
34
+
35
+
36
+            <view class="dept-selector">
37
+                <view class="dept-select-trigger" :class="{ 'dept-select-trigger-disabled': !isStationMaster()&&!isBrigade()&&!isBanzuzhang() }"
38
+                    @click="chooseDept">
39
+                    <u-icon name="list" color="#A78BFA" size="16"></u-icon>
40
+                    <text class="dept-name-text">{{ selectedDeptName || '请选择小组' }}</text>
41
+                    <u-icon name="arrow-down" color="#A78BFA" size="14"></u-icon>
42
+                </view>
43
+            </view>
44
+
45
+
46
+            <view class="tab-nav">
47
+                <view class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
48
+                    能力画像
49
+                </view>
50
+                <view class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
51
+                    运行数据
52
+                </view>
53
+            </view>
54
+        </view>
55
+
56
+        <view class="page-content">
57
+            <SectionTitle title="部门概况">
58
+                <DeptStats :statsData="deptStatsData" />
59
+            </SectionTitle>
60
+
61
+            <view v-if="activeTab === 'profile'">
62
+                <SectionTitle title="七维得分一览">
63
+                    <ProfileRadar :chartsData="radarData" />
64
+                </SectionTitle>
65
+
66
+                <SectionTitle title="团队成员">
67
+                    <TeamMemberTable :chartsData="teamMemberData" />
68
+                </SectionTitle>
69
+
70
+                <SectionTitle title="成员基本情况分布">
71
+                    <MemberBasicDistribution :chartsData="memberBasicData" />
72
+                </SectionTitle>
73
+
74
+                <SectionTitle title="成员职位情况分布">
75
+                    <MemberPositionDistribution :chartsData="memberPositionData" />
76
+                </SectionTitle>
77
+            </view>
78
+
79
+            <view v-if="activeTab === 'data'">
80
+                <SectionTitle title="当日开航每小时通道过检率">
81
+                    <PassengerChart :chartsData="passengerData" />
82
+                </SectionTitle>
83
+
84
+                <SectionTitle title="查获信息展示">
85
+                    <SeizureInfo :chartsData="seizureInfoData" />
86
+                </SectionTitle>
87
+
88
+                <SectionTitle title="查获物品分布">
89
+                    <ItemDistribution :chartsData="itemDistributionData" />
90
+                </SectionTitle>
91
+
92
+                <SectionTitle title="每日查获数量(总表)">
93
+                    <SeizedNumAll :chartsData="dailySeizureTotalData" />
94
+                </SectionTitle>
95
+
96
+                <SectionTitle title="每日查获数量">
97
+                    <DailySeizureChart :chartsData="dailySeizureData" />
98
+                </SectionTitle>
99
+
100
+                <SectionTitle title="查获工作区域分布">
101
+                    <AreaDistribution :chartsData="areaDistributionData" />
102
+                </SectionTitle>
103
+
104
+                <SectionTitle title="不安全事件物品分布">
105
+                    <UnsafeItemsChart :chartsData="unsafeItemsData" />
106
+                </SectionTitle>
107
+
108
+                <SectionTitle title="不安全事件类型分布">
109
+                    <UnsafeTypesChart :chartsData="unsafeTypesData" />
110
+                </SectionTitle>
111
+
112
+                <SectionTitle title="不安全事件岗位分布">
113
+                    <UnsafePositionChart :chartsData="unsafePositionData" />
114
+                </SectionTitle>
115
+
116
+                <SecurityTestCharts :chartsData="securityTestData" />
117
+
118
+                <SectionTitle title="各岗位监察问题分布">
119
+                    <SupervisionDistribution :chartsData="supervisionData" />
120
+                </SectionTitle>
121
+
122
+                <SectionTitle title="实时质控拦截物品分布">
123
+                    <InterceptionDistribution :chartsData="interceptionData" />
124
+                </SectionTitle>
125
+            </view>
126
+        </view>
127
+
128
+        <DeptSelector :show.sync="showDeptPicker" v-model="selectedDeptId" :options="deptList" :loading="deptLoading"
129
+            @change="onDeptSelectChange" title="小组" />
130
+    </view>
131
+</template>
132
+
133
+<script>
134
+import SectionTitle from '@/components/SectionTitle.vue'
135
+import TeamMemberTable from '../components/TeamMemberTable.vue'
136
+import MemberBasicDistribution from '../components/MemberBasicDistribution.vue'
137
+import MemberPositionDistribution from '../components/MemberPositionDistribution.vue'
138
+import PassengerChart from '../components/PassengerChart.vue'
139
+import SeizureInfo from '../components/SeizureInfo.vue'
140
+import ItemDistribution from '../components/ItemDistribution.vue'
141
+import DailySeizureChart from '../components/DailySeizureChart.vue'
142
+import AreaDistribution from '../components/AreaDistribution.vue'
143
+import UnsafeItemsChart from '../components/UnsafeItemsChart.vue'
144
+import UnsafeTypesChart from '../components/UnsafeTypesChart.vue'
145
+import UnsafePositionChart from '../components/UnsafePositionChart.vue'
146
+import SecurityTestCharts from '../components/SecurityTestCharts.vue'
147
+import ProfileRadar from '../components/ProfileRadar.vue'
148
+import SeizedNumAll from '../components/SeizedNumAll.vue'
149
+import SupervisionDistribution from '../components/SupervisionDistribution.vue'
150
+import InterceptionDistribution from '../components/InterceptionDistribution.vue'
151
+import DeptStats from '../components/DeptStats.vue'
152
+import DeptSelector from '../components/DeptSelector.vue'
153
+import {
154
+    countStationTeamStats,
155
+    getDeptMemberDistribution,
156
+    getDeptPositionDistribution,
157
+    countStationHourlyThroughput,
158
+    countSeizureInfoItem,
159
+    countSeizeSubjectCategoryQuantity,
160
+    countSeizureTotalQuantity,
161
+    countSeizureSingleQuantity,
162
+    countSeizeAreaQuantity,
163
+    countSeizureStatsItem,
164
+    countSeizureStatsType,
165
+    countSeizureStatsPost,
166
+    securityTestItemClassification,
167
+    securityTestPassingStatus,
168
+    securityTestRegion,
169
+    getDimensionScoreOverview,
170
+    countLanePeakThroughput,
171
+    realtimeInterceptionItem,
172
+    supervisionProblemPosition,
173
+    countDeptTeamStats
174
+} from '@/api/portraitManagement/portraitManagement'
175
+import { getDeptUserTree } from '@/api/system/user'
176
+
177
+const itemColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#9CA3AF', '#A78BFA', '#F472B6', '#6EE7B7']
178
+
179
+export default {
180
+    name: 'DeptProfile',
181
+    components: {
182
+        SectionTitle,
183
+        TeamMemberTable,
184
+        MemberBasicDistribution,
185
+        MemberPositionDistribution,
186
+        PassengerChart,
187
+        SeizureInfo,
188
+        ItemDistribution,
189
+        DailySeizureChart,
190
+        AreaDistribution,
191
+        UnsafeItemsChart,
192
+        UnsafeTypesChart,
193
+        UnsafePositionChart,
194
+        SecurityTestCharts,
195
+        ProfileRadar,
196
+        SeizedNumAll,
197
+        SupervisionDistribution,
198
+        InterceptionDistribution,
199
+        DeptStats,
200
+        DeptSelector
201
+    },
202
+    data() {
203
+        return {
204
+            activeTab: 'profile',
205
+            selectedTimeTag: 3,
206
+            timeTags: ['近一周', '近一月', '近三月', '近一年', '自定义时间范围'],
207
+            currentTime: '',
208
+            timer: null,
209
+            startDate: '',
210
+            endDate: '',
211
+
212
+            // 部门选择相关
213
+            selectedDeptId: null,
214
+            selectedDeptName: '',
215
+            showDeptPicker: false,
216
+            deptList: [],
217
+            deptLoading: false,
218
+            radarData: [],
219
+            teamMemberData: [],
220
+            memberBasicData: {
221
+                gender: { labels: [], data: [] },
222
+                ethnicity: { labels: [], data: [] },
223
+                political: { labels: [], data: [] }
224
+            },
225
+            memberPositionData: {
226
+                qualification: { labels: [], data: [] },
227
+                experience: { labels: [], data: [] },
228
+                position: { labels: [], data: [] }
229
+            },
230
+            passengerData: {
231
+                labels: [],
232
+                areas: {},
233
+                totalFlow: []
234
+            },
235
+            seizureInfoData: {
236
+                total: 0,
237
+                depts: {
238
+                    labels: [],
239
+                    data: []
240
+                }
241
+            },
242
+            itemDistributionData: {
243
+                items: []
244
+            },
245
+            dailySeizureTotalData: [],
246
+            dailySeizureData: {
247
+                total: {
248
+                    labels: [],
249
+                    data: []
250
+                },
251
+                dept: {
252
+                    labels: [],
253
+                    data: {}
254
+                }
255
+            },
256
+            areaDistributionData: {
257
+                labels: [],
258
+                data: []
259
+            },
260
+            unsafeItemsData: {
261
+                items: []
262
+            },
263
+            unsafeTypesData: {
264
+                types: []
265
+            },
266
+            unsafePositionData: {
267
+                total: 0,
268
+                positions: {
269
+                    labels: [],
270
+                    data: []
271
+                }
272
+            },
273
+            securityTestData: {
274
+                items: {
275
+                    labels: [],
276
+                    data: []
277
+                },
278
+                results: {
279
+                    labels: [],
280
+                    data: []
281
+                },
282
+                areas: {
283
+                    labels: [],
284
+                    data: []
285
+                }
286
+            },
287
+            supervisionData: [],
288
+            interceptionData: [],
289
+            deptStatsData: {}
290
+        }
291
+    },
292
+    computed: {
293
+        currentDate() {
294
+            const now = new Date()
295
+            const year = now.getFullYear()
296
+            const month = String(now.getMonth() + 1).padStart(2, '0')
297
+            const day = String(now.getDate()).padStart(2, '0')
298
+            const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
299
+            const weekDay = weekDays[now.getDay()]
300
+            return `${year}年${month}月${day}日 ${weekDay}`
301
+        }
302
+    },
303
+    mounted() {
304
+        this.updateTime()
305
+        this.timer = setInterval(() => {
306
+            this.updateTime()
307
+        }, 1000)
308
+        this.fetchDeptList()
309
+    },
310
+    beforeDestroy() {
311
+        if (this.timer) {
312
+            clearInterval(this.timer)
313
+        }
314
+    },
315
+    methods: {
316
+        updateTime() {
317
+            const now = new Date()
318
+            const hours = String(now.getHours()).padStart(2, '0')
319
+            const minutes = String(now.getMinutes()).padStart(2, '0')
320
+            const seconds = String(now.getSeconds()).padStart(2, '0')
321
+            this.currentTime = `${hours}:${minutes}:${seconds}`
322
+        },
323
+        // 部门选择改变
324
+        onDeptSelectChange(item) {
325
+            this.selectedDeptId = item.deptId
326
+            this.selectedDeptName = item.deptName
327
+            // 重新请求数据
328
+            const params = this.buildTimeParams()
329
+            this.fetchAllData(params)
330
+        },
331
+        getUserRoles() {
332
+            const userInfo = this.$store.state.user
333
+            return userInfo && userInfo.roles ? userInfo.roles : []
334
+        },
335
+        isStationMaster() {
336
+            const roles = this.getUserRoles()
337
+            return roles.includes('test') || roles.includes('zhijianke')
338
+        },
339
+        isBrigade() {
340
+            const roles = this.getUserRoles()
341
+            return roles.includes('bumenjingli')
342
+        },
343
+        isBanzuzhang() {
344
+            const roles = this.getUserRoles()
345
+            return roles.includes('banzuzhang')
346
+        },
347
+        chooseDept() {
348
+            
349
+            if(this.isStationMaster()||this.isBrigade()||this.isBanzuzhang()){
350
+                this.showDeptPicker = true
351
+            }
352
+        },
353
+        fetchDeptList() {
354
+            this.deptLoading = true
355
+            const userInfo = this.$store.state.user?.userInfo;
356
+            let params = this.isStationMaster() ? {  deptId: userInfo.deptId } : { deptId: userInfo.deptId }
357
+            getDeptUserTree(params).then(res => {
358
+                if (res.code === 200) {
359
+                    let allDepts = this.flattenDeptTree(res.data || [])
360
+                    this.deptList = allDepts.filter(d => d.deptType === 'TEAMS')
361
+                    if (userInfo && userInfo.deptId) {
362
+                        const dept = this.deptList.find(d => d.deptId === userInfo.deptId)
363
+                        if (dept) {
364
+                            this.selectedDeptId = dept.deptId
365
+                            this.selectedDeptName = dept.deptName
366
+                        } else if (this.deptList.length > 0) {
367
+                            this.selectedDeptId = this.deptList[0].deptId
368
+                            this.selectedDeptName = this.deptList[0].deptName
369
+                        }
370
+                    } else if (this.deptList.length > 0) {
371
+                        this.selectedDeptId = this.deptList[0].deptId
372
+                        this.selectedDeptName = this.deptList[0].deptName
373
+                    }
374
+                    // 选择完部门后请求数据
375
+                    this.onTimeTagClick(3)
376
+                }
377
+            }).catch(() => {
378
+            }).finally(() => {
379
+                this.deptLoading = false
380
+            })
381
+        },
382
+        flattenDeptTree(tree) {
383
+            const result = []
384
+            const traverse = (nodes) => {
385
+                nodes.forEach(node => {
386
+                    if (node.id) {
387
+                        result.push({
388
+                            deptId: node.id,
389
+                            deptName: node.label,
390
+                            deptType: node.deptType
391
+                        })
392
+                    }
393
+                    if (node.children && node.children.length > 0) {
394
+                        traverse(node.children)
395
+                    }
396
+                })
397
+            }
398
+            traverse(tree)
399
+            return result
400
+        },
401
+        fetchAllData(params) {
402
+            this.fetchDeptStats(params)
403
+            this.fetchRadarData(params)
404
+            this.fetchTeamData(params)
405
+            this.fetchMemberDistribution(params)
406
+            this.fetchPositionDistribution(params)
407
+            const copyParams = { startDate: params.startDate, endDate: params.endDate }
408
+            this.fetchPassengerData(copyParams)
409
+            this.fetchSeizureInfo(copyParams)
410
+            this.fetchItemDistribution(copyParams)
411
+            this.fetchDailySeizure(copyParams)
412
+            this.fetchAreaDistribution(copyParams)
413
+            this.fetchUnsafeItems(copyParams)
414
+            this.fetchUnsafeTypes(copyParams)
415
+            this.fetchUnsafePosition(copyParams)
416
+            this.fetchSecurityTestData(copyParams)
417
+            this.fetchSupervisionData(copyParams)
418
+            this.fetchInterceptionData(copyParams)
419
+        },
420
+        buildTimeParams() {
421
+            const now = new Date()
422
+            const today = this.formatDate(now)
423
+            const deptId = this.selectedDeptId || 100
424
+            if (this.selectedTimeTag === 4) {
425
+                if (this.startDate && this.endDate) {
426
+                    return { startDate: this.startDate, endDate: this.endDate, deptId }
427
+                }
428
+                return { deptId }
429
+            }
430
+            let startDate
431
+            switch (this.selectedTimeTag) {
432
+                case 0:
433
+                    startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
434
+                    break
435
+                case 1:
436
+                    startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
437
+                    break
438
+                case 2:
439
+                    startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
440
+                    break
441
+                case 3:
442
+                    startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
443
+                    break
444
+                default:
445
+                    return { deptId }
446
+            }
447
+            return { startDate: this.formatDate(startDate), endDate: today, deptId }
448
+        },
449
+        formatDate(date) {
450
+            const y = date.getFullYear()
451
+            const m = String(date.getMonth() + 1).padStart(2, '0')
452
+            const d = String(date.getDate()).padStart(2, '0')
453
+            return `${y}-${m}-${d}`
454
+        },
455
+        onTimeTagClick(index) {
456
+            this.selectedTimeTag = index
457
+            if (index === 4) {
458
+                this.startDate = ''
459
+                this.endDate = ''
460
+                return
461
+            }
462
+            const params = this.buildTimeParams()
463
+            this.fetchAllData(params)
464
+        },
465
+        onStartDateChange(e) {
466
+            this.startDate = e.detail.value
467
+            if (this.startDate && this.endDate) {
468
+                this.fetchAllData(this.buildTimeParams())
469
+            }
470
+        },
471
+        onEndDateChange(e) {
472
+            this.endDate = e.detail.value
473
+            if (this.startDate && this.endDate) {
474
+                this.fetchAllData(this.buildTimeParams())
475
+            }
476
+        },
477
+        fetchDeptStats(params) {
478
+            countDeptTeamStats(params).then(res => {
479
+                if (res.code === 200 && res.data) {
480
+                    this.deptStatsData = res.data
481
+                }
482
+            }).catch(() => { })
483
+        },
484
+        fetchRadarData(params) {
485
+            getDimensionScoreOverview(params).then(res => {
486
+                if (res.code === 200 && res.data) {
487
+                    // 支持两种数据结构
488
+                    if (res.data.dimensions && Array.isArray(res.data.dimensions)) {
489
+
490
+                        this.radarData = res.data.dimensions
491
+                    } else {
492
+                        this.radarData = []
493
+                    }
494
+                }
495
+            }).catch(() => {
496
+                this.radarData = []
497
+            })
498
+        },
499
+        fetchTeamData(params) {
500
+            countStationTeamStats(params).then(res => {
501
+                if (res.code === 200 && res.data) {
502
+                    this.teamMemberData = (res.data || []).map(item => ({
503
+                        department: item.deptName,
504
+                        employeeCount: item.employeeCount,
505
+                        partyMemberCount: item.partyMemberCount,
506
+                        avgAge: item.avgAge,
507
+                        avgTenure: item.avgWorkYears,
508
+                        certificateCount: item.qualificationLevel,
509
+                        machineYears: item.avgXrayOperatorYears,
510
+                        comprehensiveScore: item.totalScore
511
+                    }))
512
+                }
513
+            }).catch(() => { })
514
+        },
515
+        fetchMemberDistribution(params) {
516
+            getDeptMemberDistribution(params).then(res => {
517
+                if (res.code === 200 && res.data) {
518
+                    this.memberBasicData = {
519
+                        gender: {
520
+                            labels: (res.data.sexDistribution || []).map(item => item.name),
521
+                            data: (res.data.sexDistribution || []).map(item => item.count)
522
+                        },
523
+                        ethnicity: {
524
+                            labels: (res.data.nationDistribution || []).map(item => item.name),
525
+                            data: (res.data.nationDistribution || []).map(item => item.count)
526
+                        },
527
+                        political: {
528
+                            labels: (res.data.politicalDistribution || []).map(item => item.name),
529
+                            data: (res.data.politicalDistribution || []).map(item => item.count)
530
+                        }
531
+                    }
532
+                }
533
+
534
+            }).catch(() => { })
535
+        },
536
+        fetchPositionDistribution(params) {
537
+            getDeptPositionDistribution(params).then(res => {
538
+                if (res.code === 200 && res.data) {
539
+                    this.memberPositionData = {
540
+                        qualification: {
541
+                            labels: (res.data.qualificationDistribution || []).map(item => item.name),
542
+                            data: (res.data.qualificationDistribution || []).map(item => item.count)
543
+                        },
544
+                        experience: {
545
+                            labels: (res.data.xrayYearDistribution || []).map(item => item.name),
546
+                            data: (res.data.xrayYearDistribution || []).map(item => item.count)
547
+                        },
548
+                        position: {
549
+                            labels: (res.data.positionDistribution || []).map(item => item.name),
550
+                            data: (res.data.positionDistribution || []).map(item => item.count)
551
+                        }
552
+                    }
553
+                }
554
+            }).catch(() => { })
555
+        },
556
+        fetchPassengerData(params) {
557
+            countLanePeakThroughput(params).then(res => {
558
+                if (res.code === 200 && res.data) {
559
+                    const rawData = res.data || []
560
+                    const hours = [...new Set(rawData.map(item => item.hour))]
561
+                    const areaMap = {}
562
+                    rawData.forEach(item => {
563
+                        (item.laneList || []).forEach(lane => {
564
+                            if (!areaMap[lane.laneName]) {
565
+                                areaMap[lane.laneName] = []
566
+                            }
567
+                            areaMap[lane.laneName].push(lane.throughputRate)
568
+                        })
569
+                    })
570
+                    this.passengerData = {
571
+                        labels: hours,
572
+                        areas: areaMap,
573
+                        totalFlow: rawData.map(item => item.totalLaneThroughput)
574
+                    }
575
+                }
576
+            }).catch(() => { })
577
+        },
578
+        fetchSeizureInfo(params) {
579
+            countSeizureInfoItem(params).then(res => {
580
+                if (res.code === 200 && res.data) {
581
+                    const itemList = res.data.itemList || []
582
+                    this.seizureInfoData = {
583
+                        total: res.data.totalSeizeNum || 0,
584
+                        depts: {
585
+                            labels: itemList.map(item => item.name),
586
+                            data: itemList.map(item => item.seizeNum)
587
+                        }
588
+                    }
589
+                }
590
+            }).catch(() => { })
591
+        },
592
+        fetchItemDistribution(params) {
593
+            countSeizeSubjectCategoryQuantity(params).then(res => {
594
+                if (res.code === 200 && res.data) {
595
+                    const items = (res.data || []).map((item, index) => ({
596
+                        name: item.itemName,
597
+                        value: item.itemNum,
598
+                        color: itemColors[index % itemColors.length]
599
+                    }))
600
+                    this.itemDistributionData = { items }
601
+                }
602
+            }).catch(() => { })
603
+        },
604
+        fetchDailySeizure(params) {
605
+            countSeizureTotalQuantity(params).then(res => {
606
+                if (res.code === 200 && res.data) {
607
+                    this.dailySeizureTotalData = res.data || []
608
+                    this.dailySeizureData.total = {
609
+                        labels: (res.data || []).map(item => item.recordDate),
610
+                        data: (res.data || []).map(item => item.seizeQuantity)
611
+                    }
612
+                }
613
+            }).catch(() => { })
614
+            countSeizureSingleQuantity(params).then(res => {
615
+                if (res.code === 200 && res.data) {
616
+                    const rawData = res.data || []
617
+                    const dates = rawData.map(item => item.recordDate)
618
+                    const deptMap = {}
619
+                    rawData.forEach(dayItem => {
620
+                        (dayItem.items || []).forEach(subItem => {
621
+                            if (!deptMap[subItem.groupName]) {
622
+                                deptMap[subItem.groupName] = []
623
+                            }
624
+                            deptMap[subItem.groupName].push(subItem.seizeQuantity)
625
+                        })
626
+                    })
627
+                    this.dailySeizureData.dept = {
628
+                        labels: dates,
629
+                        deptData: Object.keys(deptMap).map(name => ({
630
+                            name,
631
+                            data: deptMap[name]
632
+                        }))
633
+                    }
634
+                }
635
+            }).catch(() => { })
636
+        },
637
+        fetchAreaDistribution(params) {
638
+            countSeizeAreaQuantity(params).then(res => {
639
+                if (res.code === 200 && res.data) {
640
+                    const data = res.data || []
641
+                    this.areaDistributionData = {
642
+                        labels: data.map(item => item.workArea),
643
+                        data: data.map(item => item.areaSeizeNum)
644
+                    }
645
+                }
646
+            }).catch(() => { })
647
+        },
648
+        fetchUnsafeItems(params) {
649
+            countSeizureStatsItem(params).then(res => {
650
+                if (res.code === 200 && res.data) {
651
+                    const data = res.data || []
652
+                    this.unsafeItemsData = {
653
+                        items: data.map(item => ({
654
+                            name: item.itemName,
655
+                            value: item.itemNum
656
+                        }))
657
+                    }
658
+                }
659
+            }).catch(() => { })
660
+        },
661
+        fetchUnsafeTypes(params) {
662
+            countSeizureStatsType(params).then(res => {
663
+                if (res.code === 200 && res.data) {
664
+                    const data = res.data || []
665
+                    this.unsafeTypesData = {
666
+                        types: data.map(item => ({
667
+                            name: item.eventType,
668
+                            value: item.eventTypeNum
669
+                        }))
670
+                    }
671
+                }
672
+            }).catch(() => { })
673
+        },
674
+        fetchUnsafePosition(params) {
675
+            countSeizureStatsPost(params).then(res => {
676
+                if (res.code === 200 && res.data) {
677
+                    const data = res.data || []
678
+                    const total = data.reduce((sum, item) => sum + (item.count || 0), 0)
679
+                    this.unsafePositionData = {
680
+                        total: total,
681
+                        positions: {
682
+                            labels: data.map(item => item.positionName),
683
+                            data: data.map(item => item.positionNum)
684
+                        }
685
+                    }
686
+                }
687
+            }).catch(() => { })
688
+        },
689
+        fetchSecurityTestData(params) {
690
+            securityTestItemClassification(params).then(res => {
691
+                if (res.code === 200 && res.data) {
692
+                    const data = res.data || []
693
+                    this.securityTestData.items = {
694
+                        labels: data.map(item => item.name),
695
+                        data: data.map(item => item.total)
696
+                    }
697
+                }
698
+            }).catch(() => { })
699
+            securityTestPassingStatus(params).then(res => {
700
+                if (res.code === 200 && res.data) {
701
+                    const data = res.data || []
702
+                    this.securityTestData.results = {
703
+                        labels: data.map(item => item.name),
704
+                        data: data.map(item => item.total)
705
+                    }
706
+                }
707
+            }).catch(() => { })
708
+            securityTestRegion(params).then(res => {
709
+                if (res.code === 200 && res.data) {
710
+                    const data = res.data || []
711
+                    this.securityTestData.areas = {
712
+                        labels: data.map(item => item.name),
713
+                        data: data.map(item => item.total)
714
+                    }
715
+                }
716
+            }).catch(() => { })
717
+        },
718
+        fetchSupervisionData(params) {
719
+            supervisionProblemPosition(params).then(res => {
720
+                if (res.code === 200 && res.data) {
721
+                    this.supervisionData = (res.data || []).map(item => ({
722
+                        num: item.total,
723
+                        name: item.name
724
+                    }))
725
+                }
726
+            }).catch(() => { })
727
+        },
728
+        fetchInterceptionData(params) {
729
+            realtimeInterceptionItem(params).then(res => {
730
+                if (res.code === 200 && res.data) {
731
+                    this.interceptionData = (res.data || []).map(item => ({
732
+                        num: item.total,
733
+                        name: item.name
734
+                    }))
735
+                }
736
+            }).catch(() => { })
737
+        }
738
+    }
739
+}
740
+</script>
741
+
742
+<style lang="scss" scoped>
743
+    .dept-selector {
744
+    padding: 16rpx 32rpx;
745
+    background: rgba(30, 27, 75, 0.8);
746
+}
747
+
748
+.dept-select-trigger {
749
+    display: flex;
750
+    align-items: center;
751
+    justify-content: center;
752
+    gap: 12rpx;
753
+    padding: 16rpx 24rpx;
754
+    background: rgba(45, 42, 85, 0.8);
755
+    border: 1rpx solid rgba(167, 139, 250, 0.3);
756
+    border-radius: 50rpx;
757
+}
758
+
759
+.dept-select-trigger-disabled {
760
+    opacity: 0.5;
761
+    pointer-events: none;
762
+}
763
+
764
+.dept-name-text {
765
+    font-size: 26rpx;
766
+    color: rgba(255, 255, 255, 0.9);
767
+}
768
+
769
+.dept-profile-page {
770
+    min-height: 100vh;
771
+    background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
772
+    padding-bottom: 40rpx;
773
+}
774
+
775
+.page-header {
776
+    position: sticky;
777
+    top: 0;
778
+    z-index: 100;
779
+    background: rgba(30, 27, 75, 0.9);
780
+    backdrop-filter: blur(10px);
781
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
782
+}
783
+
784
+.header-title {
785
+    padding: 24rpx 32rpx;
786
+    display: flex;
787
+    justify-content: space-between;
788
+    align-items: center;
789
+
790
+    .title-main {
791
+        font-size: 36rpx;
792
+        font-weight: bold;
793
+        background: linear-gradient(90deg, #A78BFA, #60A5FA);
794
+        -webkit-background-clip: text;
795
+        -webkit-text-fill-color: transparent;
796
+        background-clip: text;
797
+    }
798
+}
799
+
800
+.header-right {
801
+    display: flex;
802
+    align-items: center;
803
+    gap: 16rpx;
804
+
805
+    .current-time {
806
+        font-size: 24rpx;
807
+        color: rgba(255, 255, 255, 0.6);
808
+    }
809
+}
810
+
811
+.time-filter {
812
+    padding: 16rpx 32rpx;
813
+    background: rgba(30, 27, 75, 0.8);
814
+}
815
+
816
+.time-scroll {
817
+    width: 100%;
818
+}
819
+
820
+.time-tags {
821
+    display: flex;
822
+    gap: 16rpx;
823
+    white-space: nowrap;
824
+}
825
+
826
+.time-tag {
827
+    padding: 8rpx 10rpx;
828
+    border-radius: 50rpx;
829
+    background: rgba(45, 42, 85, 0.8);
830
+    font-size: 24rpx;
831
+    color: rgba(255, 255, 255, 0.6);
832
+    transition: all 0.3s;
833
+
834
+    &.active {
835
+        background: #A78BFA;
836
+        color: #1E1B4B;
837
+        font-weight: 500;
838
+    }
839
+}
840
+
841
+.date-range-picker {
842
+    display: flex;
843
+    align-items: center;
844
+    gap: 16rpx;
845
+    margin-top: 16rpx;
846
+    padding-top: 16rpx;
847
+    border-top: 1rpx solid rgba(255, 255, 255, 0.1);
848
+}
849
+
850
+.date-input {
851
+    flex: 1;
852
+    padding: 12rpx 24rpx;
853
+    border-radius: 12rpx;
854
+    background: rgba(45, 42, 85, 0.8);
855
+    font-size: 24rpx;
856
+    color: rgba(255, 255, 255, 0.4);
857
+    text-align: center;
858
+
859
+    &.filled {
860
+        color: rgba(255, 255, 255, 0.9);
861
+    }
862
+}
863
+
864
+.date-separator {
865
+    font-size: 24rpx;
866
+    color: rgba(255, 255, 255, 0.5);
867
+    flex-shrink: 0;
868
+}
869
+
870
+.tab-nav {
871
+    padding: 16rpx 32rpx;
872
+    display: flex;
873
+    justify-content: center;
874
+    gap: 64rpx;
875
+}
876
+
877
+.tab-item {
878
+    font-size: 28rpx;
879
+    color: rgba(255, 255, 255, 0.5);
880
+    padding-bottom: 8rpx;
881
+    border-bottom: 2rpx solid transparent;
882
+    transition: all 0.3s;
883
+
884
+    &.active {
885
+        color: #A78BFA;
886
+        border-bottom-color: #A78BFA;
887
+    }
888
+}
889
+
890
+.page-content {
891
+    padding: 32rpx;
892
+    display: flex;
893
+    flex-direction: column;
894
+}
895
+</style>

+ 898 - 0
src/pages/teamProfile/index.vue

@@ -0,0 +1,898 @@
1
+<template>
2
+    <view class="dept-profile-page">
3
+        <view class="page-header">
4
+            <view class="header-title">
5
+                <view class="title-main">班组综合信息展示</view>
6
+                <view class="header-right">
7
+                    <view class="current-time">{{ currentTime }}</view>
8
+                </view>
9
+            </view>
10
+
11
+            <view class="time-filter">
12
+                <scroll-view scroll-x class="time-scroll">
13
+                    <view class="time-tags">
14
+                        <view v-for="(tag, index) in timeTags" :key="index"
15
+                            :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
16
+                            {{ tag }}
17
+                        </view>
18
+                    </view>
19
+                </scroll-view>
20
+                <view v-if="selectedTimeTag === 4" class="date-range-picker">
21
+                    <picker mode="date" :value="startDate" @change="onStartDateChange">
22
+                        <view class="date-input" :class="{ filled: startDate }">
23
+                            {{ startDate || '开始日期' }}
24
+                        </view>
25
+                    </picker>
26
+                    <text class="date-separator">至</text>
27
+                    <picker mode="date" :value="endDate" @change="onEndDateChange">
28
+                        <view class="date-input" :class="{ filled: endDate }">
29
+                            {{ endDate || '结束日期' }}
30
+                        </view>
31
+                    </picker>
32
+                </view>
33
+            </view>
34
+
35
+
36
+            <view class="dept-selector">
37
+                <view class="dept-select-trigger" :class="{ 'dept-select-trigger-disabled': !isBrigade()&&!isStationMaster() }"
38
+                    @click="chooseDept">
39
+                    <u-icon name="list" color="#A78BFA" size="16"></u-icon>
40
+                    <text class="dept-name-text">{{ selectedDeptName || '请选择班组' }}</text>
41
+                    <u-icon name="arrow-down" color="#A78BFA" size="14"></u-icon>
42
+                </view>
43
+            </view>
44
+
45
+
46
+            <view class="tab-nav">
47
+                <view class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
48
+                    能力画像
49
+                </view>
50
+                <view class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
51
+                    运行数据
52
+                </view>
53
+            </view>
54
+        </view>
55
+
56
+        <view class="page-content">
57
+            <SectionTitle title="部门概况">
58
+                <DeptStats :statsData="deptStatsData" />
59
+            </SectionTitle>
60
+
61
+            <view v-if="activeTab === 'profile'">
62
+                <SectionTitle title="七维得分一览">
63
+                    <ProfileRadar :chartsData="radarData" />
64
+                </SectionTitle>
65
+
66
+                <SectionTitle title="团队成员">
67
+                    <TeamMemberTable :chartsData="teamMemberData" />
68
+                </SectionTitle>
69
+
70
+                <SectionTitle title="成员基本情况分布">
71
+                    <MemberBasicDistribution :chartsData="memberBasicData" />
72
+                </SectionTitle>
73
+
74
+                <SectionTitle title="成员职位情况分布">
75
+                    <MemberPositionDistribution :chartsData="memberPositionData" />
76
+                </SectionTitle>
77
+            </view>
78
+
79
+            <view v-if="activeTab === 'data'">
80
+                <SectionTitle title="当日开航每小时通道过检率">
81
+                    <PassengerChart :chartsData="passengerData" />
82
+                </SectionTitle>
83
+
84
+                <SectionTitle title="查获信息展示">
85
+                    <SeizureInfo :chartsData="seizureInfoData" />
86
+                </SectionTitle>
87
+
88
+                <SectionTitle title="查获物品分布">
89
+                    <ItemDistribution :chartsData="itemDistributionData" />
90
+                </SectionTitle>
91
+
92
+                <SectionTitle title="每日查获数量(总表)">
93
+                    <SeizedNumAll :chartsData="dailySeizureTotalData" />
94
+                </SectionTitle>
95
+
96
+                <SectionTitle title="每日查获数量">
97
+                    <DailySeizureChart :chartsData="dailySeizureData" />
98
+                </SectionTitle>
99
+
100
+                <SectionTitle title="查获工作区域分布">
101
+                    <AreaDistribution :chartsData="areaDistributionData" />
102
+                </SectionTitle>
103
+
104
+                <SectionTitle title="不安全事件物品分布">
105
+                    <UnsafeItemsChart :chartsData="unsafeItemsData" />
106
+                </SectionTitle>
107
+
108
+                <SectionTitle title="不安全事件类型分布">
109
+                    <UnsafeTypesChart :chartsData="unsafeTypesData" />
110
+                </SectionTitle>
111
+
112
+                <SectionTitle title="不安全事件岗位分布">
113
+                    <UnsafePositionChart :chartsData="unsafePositionData" />
114
+                </SectionTitle>
115
+
116
+                <SecurityTestCharts :chartsData="securityTestData" />
117
+
118
+                <SectionTitle title="各岗位监察问题分布">
119
+                    <SupervisionDistribution :chartsData="supervisionData" />
120
+                </SectionTitle>
121
+
122
+                <SectionTitle title="实时质控拦截物品分布">
123
+                    <InterceptionDistribution :chartsData="interceptionData" />
124
+                </SectionTitle>
125
+            </view>
126
+        </view>
127
+
128
+        <DeptSelector :show.sync="showDeptPicker" v-model="selectedDeptId" :options="deptList" :loading="deptLoading"
129
+            @change="onDeptSelectChange" title="班组" />
130
+    </view>
131
+</template>
132
+
133
+<script>
134
+import SectionTitle from '@/components/SectionTitle.vue'
135
+import TeamMemberTable from '../components/TeamMemberTable.vue'
136
+import MemberBasicDistribution from '../components/MemberBasicDistribution.vue'
137
+import MemberPositionDistribution from '../components/MemberPositionDistribution.vue'
138
+import PassengerChart from '../components/PassengerChart.vue'
139
+import SeizureInfo from '../components/SeizureInfo.vue'
140
+import ItemDistribution from '../components/ItemDistribution.vue'
141
+import DailySeizureChart from '../components/DailySeizureChart.vue'
142
+import AreaDistribution from '../components/AreaDistribution.vue'
143
+import UnsafeItemsChart from '../components/UnsafeItemsChart.vue'
144
+import UnsafeTypesChart from '../components/UnsafeTypesChart.vue'
145
+import UnsafePositionChart from '../components/UnsafePositionChart.vue'
146
+import SecurityTestCharts from '../components/SecurityTestCharts.vue'
147
+import ProfileRadar from '../components/ProfileRadar.vue'
148
+import SeizedNumAll from '../components/SeizedNumAll.vue'
149
+import SupervisionDistribution from '../components/SupervisionDistribution.vue'
150
+import InterceptionDistribution from '../components/InterceptionDistribution.vue'
151
+import DeptStats from '../components/DeptStats.vue'
152
+import DeptSelector from '../components/DeptSelector.vue'
153
+import {
154
+    countStationTeamStats,
155
+    getDeptMemberDistribution,
156
+    getDeptPositionDistribution,
157
+    countStationHourlyThroughput,
158
+    countSeizureInfoItem,
159
+    countSeizeSubjectCategoryQuantity,
160
+    countSeizureTotalQuantity,
161
+    countSeizureSingleQuantity,
162
+    countSeizeAreaQuantity,
163
+    countSeizureStatsItem,
164
+    countSeizureStatsType,
165
+    countSeizureStatsPost,
166
+    securityTestItemClassification,
167
+    securityTestPassingStatus,
168
+    securityTestRegion,
169
+    getDimensionScoreOverview,
170
+    countLanePeakThroughput,
171
+    realtimeInterceptionItem,
172
+    supervisionProblemPosition,
173
+    countDeptTeamStats
174
+} from '@/api/portraitManagement/portraitManagement'
175
+import { getDeptUserTree } from '@/api/system/user'
176
+
177
+const itemColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#9CA3AF', '#A78BFA', '#F472B6', '#6EE7B7']
178
+
179
+export default {
180
+    name: 'DeptProfile',
181
+    components: {
182
+        SectionTitle,
183
+        TeamMemberTable,
184
+        MemberBasicDistribution,
185
+        MemberPositionDistribution,
186
+        PassengerChart,
187
+        SeizureInfo,
188
+        ItemDistribution,
189
+        DailySeizureChart,
190
+        AreaDistribution,
191
+        UnsafeItemsChart,
192
+        UnsafeTypesChart,
193
+        UnsafePositionChart,
194
+        SecurityTestCharts,
195
+        ProfileRadar,
196
+        SeizedNumAll,
197
+        SupervisionDistribution,
198
+        InterceptionDistribution,
199
+        DeptStats,
200
+        DeptSelector
201
+    },
202
+    data() {
203
+        return {
204
+            activeTab: 'profile',
205
+            selectedTimeTag: 3,
206
+            timeTags: ['近一周', '近一月', '近三月', '近一年', '自定义时间范围'],
207
+            currentTime: '',
208
+            timer: null,
209
+            startDate: '',
210
+            endDate: '',
211
+
212
+            // 部门选择相关
213
+            selectedDeptId: null,
214
+            selectedDeptName: '',
215
+            showDeptPicker: false,
216
+            deptList: [],
217
+            deptLoading: false,
218
+            radarData: [],
219
+            teamMemberData: [],
220
+            memberBasicData: {
221
+                gender: { labels: [], data: [] },
222
+                ethnicity: { labels: [], data: [] },
223
+                political: { labels: [], data: [] }
224
+            },
225
+            memberPositionData: {
226
+                qualification: { labels: [], data: [] },
227
+                experience: { labels: [], data: [] },
228
+                position: { labels: [], data: [] }
229
+            },
230
+            passengerData: {
231
+                labels: [],
232
+                areas: {},
233
+                totalFlow: []
234
+            },
235
+            seizureInfoData: {
236
+                total: 0,
237
+                depts: {
238
+                    labels: [],
239
+                    data: []
240
+                }
241
+            },
242
+            itemDistributionData: {
243
+                items: []
244
+            },
245
+            dailySeizureTotalData: [],
246
+            dailySeizureData: {
247
+                total: {
248
+                    labels: [],
249
+                    data: []
250
+                },
251
+                dept: {
252
+                    labels: [],
253
+                    data: {}
254
+                }
255
+            },
256
+            areaDistributionData: {
257
+                labels: [],
258
+                data: []
259
+            },
260
+            unsafeItemsData: {
261
+                items: []
262
+            },
263
+            unsafeTypesData: {
264
+                types: []
265
+            },
266
+            unsafePositionData: {
267
+                total: 0,
268
+                positions: {
269
+                    labels: [],
270
+                    data: []
271
+                }
272
+            },
273
+            securityTestData: {
274
+                items: {
275
+                    labels: [],
276
+                    data: []
277
+                },
278
+                results: {
279
+                    labels: [],
280
+                    data: []
281
+                },
282
+                areas: {
283
+                    labels: [],
284
+                    data: []
285
+                }
286
+            },
287
+            supervisionData: [],
288
+            interceptionData: [],
289
+            deptStatsData: {}
290
+        }
291
+    },
292
+    computed: {
293
+        currentDate() {
294
+            const now = new Date()
295
+            const year = now.getFullYear()
296
+            const month = String(now.getMonth() + 1).padStart(2, '0')
297
+            const day = String(now.getDate()).padStart(2, '0')
298
+            const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
299
+            const weekDay = weekDays[now.getDay()]
300
+            return `${year}年${month}月${day}日 ${weekDay}`
301
+        }
302
+    },
303
+    mounted() {
304
+        this.updateTime()
305
+        this.timer = setInterval(() => {
306
+            this.updateTime()
307
+        }, 1000)
308
+        this.fetchDeptList()
309
+    },
310
+    beforeDestroy() {
311
+        if (this.timer) {
312
+            clearInterval(this.timer)
313
+        }
314
+    },
315
+    methods: {
316
+        updateTime() {
317
+            const now = new Date()
318
+            const hours = String(now.getHours()).padStart(2, '0')
319
+            const minutes = String(now.getMinutes()).padStart(2, '0')
320
+            const seconds = String(now.getSeconds()).padStart(2, '0')
321
+            this.currentTime = `${hours}:${minutes}:${seconds}`
322
+        },
323
+        // 部门选择改变
324
+        onDeptSelectChange(item) {
325
+            this.selectedDeptId = item.deptId
326
+            this.selectedDeptName = item.deptName
327
+            // 重新请求数据
328
+            const params = this.buildTimeParams()
329
+            this.fetchAllData(params)
330
+        },
331
+        getUserRoles() {
332
+            const userInfo = this.$store.state.user
333
+            return userInfo && userInfo.roles ? userInfo.roles : []
334
+        },
335
+        //站长
336
+        isStationMaster() {
337
+            const roles = this.getUserRoles()
338
+            return roles.includes('test') || roles.includes('zhijianke')
339
+        },
340
+        //部门经理
341
+        isBrigade() {
342
+            const roles = this.getUserRoles()
343
+            return roles.includes('bumenjingli')
344
+        },
345
+        //班组长
346
+        // isManager() {
347
+        //     const roles = this.getUserRoles()
348
+        //     return roles.includes('banzuzhang')
349
+        // },
350
+        chooseDept() {
351
+            if (this.isStationMaster() || this.isBrigade()) {
352
+                this.showDeptPicker = true
353
+            }
354
+
355
+        },
356
+        fetchDeptList() {
357
+            this.deptLoading = true
358
+            const userInfo = this.$store.state.user?.userInfo;
359
+            let params = this.isBrigade() ? { deptId: userInfo.deptId } : { deptId: userInfo.deptId }
360
+            getDeptUserTree(params).then(res => {
361
+                if (res.code === 200) {
362
+                    let allDepts = this.flattenDeptTree(res.data || [])
363
+                    this.deptList = allDepts.filter(d => d.deptType === 'MANAGER')
364
+                    if (userInfo && userInfo.deptId) {
365
+                        const dept = this.deptList.find(d => d.deptId === userInfo.deptId)
366
+                        if (dept) {
367
+                            this.selectedDeptId = dept.deptId
368
+                            this.selectedDeptName = dept.deptName
369
+                        } else if (this.deptList.length > 0) {
370
+                            this.selectedDeptId = this.deptList[0].deptId
371
+                            this.selectedDeptName = this.deptList[0].deptName
372
+                        }
373
+                    } else if (this.deptList.length > 0) {
374
+                        this.selectedDeptId = this.deptList[0].deptId
375
+                        this.selectedDeptName = this.deptList[0].deptName
376
+                    }
377
+                    // 选择完部门后请求数据
378
+                    this.onTimeTagClick(3)
379
+                }
380
+            }).catch(() => {
381
+            }).finally(() => {
382
+                this.deptLoading = false
383
+            })
384
+        },
385
+        flattenDeptTree(tree) {
386
+            const result = []
387
+            const traverse = (nodes) => {
388
+                nodes.forEach(node => {
389
+                    if (node.id) {
390
+                        result.push({
391
+                            deptId: node.id,
392
+                            deptName: node.label,
393
+                            deptType: node.deptType
394
+                        })
395
+                    }
396
+                    if (node.children && node.children.length > 0) {
397
+                        traverse(node.children)
398
+                    }
399
+                })
400
+            }
401
+            traverse(tree)
402
+            return result
403
+        },
404
+        fetchAllData(params) {
405
+            this.fetchDeptStats(params)
406
+            this.fetchRadarData(params)
407
+            this.fetchTeamData(params)
408
+            this.fetchMemberDistribution(params)
409
+            this.fetchPositionDistribution(params)
410
+            const copyParams = { startDate: params.startDate, endDate: params.endDate }
411
+            this.fetchPassengerData(copyParams)
412
+            this.fetchSeizureInfo(copyParams)
413
+            this.fetchItemDistribution(copyParams)
414
+            this.fetchDailySeizure(copyParams)
415
+            this.fetchAreaDistribution(copyParams)
416
+            this.fetchUnsafeItems(copyParams)
417
+            this.fetchUnsafeTypes(copyParams)
418
+            this.fetchUnsafePosition(copyParams)
419
+            this.fetchSecurityTestData(copyParams)
420
+            this.fetchSupervisionData(copyParams)
421
+            this.fetchInterceptionData(copyParams)
422
+        },
423
+        buildTimeParams() {
424
+            const now = new Date()
425
+            const today = this.formatDate(now)
426
+            const deptId = this.selectedDeptId || 100
427
+            if (this.selectedTimeTag === 4) {
428
+                if (this.startDate && this.endDate) {
429
+                    return { startDate: this.startDate, endDate: this.endDate, deptId }
430
+                }
431
+                return { deptId }
432
+            }
433
+            let startDate
434
+            switch (this.selectedTimeTag) {
435
+                case 0:
436
+                    startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
437
+                    break
438
+                case 1:
439
+                    startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
440
+                    break
441
+                case 2:
442
+                    startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
443
+                    break
444
+                case 3:
445
+                    startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
446
+                    break
447
+                default:
448
+                    return { deptId }
449
+            }
450
+            return { startDate: this.formatDate(startDate), endDate: today, deptId }
451
+        },
452
+        formatDate(date) {
453
+            const y = date.getFullYear()
454
+            const m = String(date.getMonth() + 1).padStart(2, '0')
455
+            const d = String(date.getDate()).padStart(2, '0')
456
+            return `${y}-${m}-${d}`
457
+        },
458
+        onTimeTagClick(index) {
459
+            this.selectedTimeTag = index
460
+            if (index === 4) {
461
+                this.startDate = ''
462
+                this.endDate = ''
463
+                return
464
+            }
465
+            const params = this.buildTimeParams()
466
+            this.fetchAllData(params)
467
+        },
468
+        onStartDateChange(e) {
469
+            this.startDate = e.detail.value
470
+            if (this.startDate && this.endDate) {
471
+                this.fetchAllData(this.buildTimeParams())
472
+            }
473
+        },
474
+        onEndDateChange(e) {
475
+            this.endDate = e.detail.value
476
+            if (this.startDate && this.endDate) {
477
+                this.fetchAllData(this.buildTimeParams())
478
+            }
479
+        },
480
+        fetchDeptStats(params) {
481
+            countDeptTeamStats(params).then(res => {
482
+                if (res.code === 200 && res.data) {
483
+                    this.deptStatsData = res.data
484
+                }
485
+            }).catch(() => { })
486
+        },
487
+        fetchRadarData(params) {
488
+            getDimensionScoreOverview(params).then(res => {
489
+                if (res.code === 200 && res.data) {
490
+                    // 支持两种数据结构
491
+                    if (res.data.dimensions && Array.isArray(res.data.dimensions)) {
492
+
493
+                        this.radarData = res.data.dimensions
494
+                    } else {
495
+                        this.radarData = []
496
+                    }
497
+                }
498
+            }).catch(() => {
499
+                this.radarData = []
500
+            })
501
+        },
502
+        fetchTeamData(params) {
503
+            countStationTeamStats(params).then(res => {
504
+                if (res.code === 200 && res.data) {
505
+                    this.teamMemberData = (res.data || []).map(item => ({
506
+                        department: item.deptName,
507
+                        employeeCount: item.employeeCount,
508
+                        partyMemberCount: item.partyMemberCount,
509
+                        avgAge: item.avgAge,
510
+                        avgTenure: item.avgWorkYears,
511
+                        certificateCount: item.qualificationLevel,
512
+                        machineYears: item.avgXrayOperatorYears,
513
+                        comprehensiveScore: item.totalScore
514
+                    }))
515
+                }
516
+            }).catch(() => { })
517
+        },
518
+        fetchMemberDistribution(params) {
519
+            getDeptMemberDistribution(params).then(res => {
520
+                if (res.code === 200 && res.data) {
521
+                    this.memberBasicData = {
522
+                        gender: {
523
+                            labels: (res.data.sexDistribution || []).map(item => item.name),
524
+                            data: (res.data.sexDistribution || []).map(item => item.count)
525
+                        },
526
+                        ethnicity: {
527
+                            labels: (res.data.nationDistribution || []).map(item => item.name),
528
+                            data: (res.data.nationDistribution || []).map(item => item.count)
529
+                        },
530
+                        political: {
531
+                            labels: (res.data.politicalDistribution || []).map(item => item.name),
532
+                            data: (res.data.politicalDistribution || []).map(item => item.count)
533
+                        }
534
+                    }
535
+                }
536
+
537
+            }).catch(() => { })
538
+        },
539
+        fetchPositionDistribution(params) {
540
+            getDeptPositionDistribution(params).then(res => {
541
+                if (res.code === 200 && res.data) {
542
+                    this.memberPositionData = {
543
+                        qualification: {
544
+                            labels: (res.data.qualificationDistribution || []).map(item => item.name),
545
+                            data: (res.data.qualificationDistribution || []).map(item => item.count)
546
+                        },
547
+                        experience: {
548
+                            labels: (res.data.xrayYearDistribution || []).map(item => item.name),
549
+                            data: (res.data.xrayYearDistribution || []).map(item => item.count)
550
+                        },
551
+                        position: {
552
+                            labels: (res.data.positionDistribution || []).map(item => item.name),
553
+                            data: (res.data.positionDistribution || []).map(item => item.count)
554
+                        }
555
+                    }
556
+                }
557
+            }).catch(() => { })
558
+        },
559
+        fetchPassengerData(params) {
560
+            countLanePeakThroughput(params).then(res => {
561
+                if (res.code === 200 && res.data) {
562
+                    const rawData = res.data || []
563
+                    const hours = [...new Set(rawData.map(item => item.hour))]
564
+                    const areaMap = {}
565
+                    rawData.forEach(item => {
566
+                        (item.laneList || []).forEach(lane => {
567
+                            if (!areaMap[lane.laneName]) {
568
+                                areaMap[lane.laneName] = []
569
+                            }
570
+                            areaMap[lane.laneName].push(lane.throughputRate)
571
+                        })
572
+                    })
573
+                    this.passengerData = {
574
+                        labels: hours,
575
+                        areas: areaMap,
576
+                        totalFlow: rawData.map(item => item.totalLaneThroughput)
577
+                    }
578
+                }
579
+            }).catch(() => { })
580
+        },
581
+        fetchSeizureInfo(params) {
582
+            countSeizureInfoItem(params).then(res => {
583
+                if (res.code === 200 && res.data) {
584
+                    const itemList = res.data.itemList || []
585
+                    this.seizureInfoData = {
586
+                        total: res.data.totalSeizeNum || 0,
587
+                        depts: {
588
+                            labels: itemList.map(item => item.name),
589
+                            data: itemList.map(item => item.seizeNum)
590
+                        }
591
+                    }
592
+                }
593
+            }).catch(() => { })
594
+        },
595
+        fetchItemDistribution(params) {
596
+            countSeizeSubjectCategoryQuantity(params).then(res => {
597
+                if (res.code === 200 && res.data) {
598
+                    const items = (res.data || []).map((item, index) => ({
599
+                        name: item.itemName,
600
+                        value: item.itemNum,
601
+                        color: itemColors[index % itemColors.length]
602
+                    }))
603
+                    this.itemDistributionData = { items }
604
+                }
605
+            }).catch(() => { })
606
+        },
607
+        fetchDailySeizure(params) {
608
+            countSeizureTotalQuantity(params).then(res => {
609
+                if (res.code === 200 && res.data) {
610
+                    this.dailySeizureTotalData = res.data || []
611
+                    this.dailySeizureData.total = {
612
+                        labels: (res.data || []).map(item => item.recordDate),
613
+                        data: (res.data || []).map(item => item.seizeQuantity)
614
+                    }
615
+                }
616
+            }).catch(() => { })
617
+            countSeizureSingleQuantity(params).then(res => {
618
+                if (res.code === 200 && res.data) {
619
+                    const rawData = res.data || []
620
+                    const dates = rawData.map(item => item.recordDate)
621
+                    const deptMap = {}
622
+                    rawData.forEach(dayItem => {
623
+                        (dayItem.items || []).forEach(subItem => {
624
+                            if (!deptMap[subItem.groupName]) {
625
+                                deptMap[subItem.groupName] = []
626
+                            }
627
+                            deptMap[subItem.groupName].push(subItem.seizeQuantity)
628
+                        })
629
+                    })
630
+                    this.dailySeizureData.dept = {
631
+                        labels: dates,
632
+                        deptData: Object.keys(deptMap).map(name => ({
633
+                            name,
634
+                            data: deptMap[name]
635
+                        }))
636
+                    }
637
+                }
638
+            }).catch(() => { })
639
+        },
640
+        fetchAreaDistribution(params) {
641
+            countSeizeAreaQuantity(params).then(res => {
642
+                if (res.code === 200 && res.data) {
643
+                    const data = res.data || []
644
+                    this.areaDistributionData = {
645
+                        labels: data.map(item => item.workArea),
646
+                        data: data.map(item => item.areaSeizeNum)
647
+                    }
648
+                }
649
+            }).catch(() => { })
650
+        },
651
+        fetchUnsafeItems(params) {
652
+            countSeizureStatsItem(params).then(res => {
653
+                if (res.code === 200 && res.data) {
654
+                    const data = res.data || []
655
+                    this.unsafeItemsData = {
656
+                        items: data.map(item => ({
657
+                            name: item.itemName,
658
+                            value: item.itemNum
659
+                        }))
660
+                    }
661
+                }
662
+            }).catch(() => { })
663
+        },
664
+        fetchUnsafeTypes(params) {
665
+            countSeizureStatsType(params).then(res => {
666
+                if (res.code === 200 && res.data) {
667
+                    const data = res.data || []
668
+                    this.unsafeTypesData = {
669
+                        types: data.map(item => ({
670
+                            name: item.eventType,
671
+                            value: item.eventTypeNum
672
+                        }))
673
+                    }
674
+                }
675
+            }).catch(() => { })
676
+        },
677
+        fetchUnsafePosition(params) {
678
+            countSeizureStatsPost(params).then(res => {
679
+                if (res.code === 200 && res.data) {
680
+                    const data = res.data || []
681
+                    const total = data.reduce((sum, item) => sum + (item.count || 0), 0)
682
+                    this.unsafePositionData = {
683
+                        total: total,
684
+                        positions: {
685
+                            labels: data.map(item => item.positionName),
686
+                            data: data.map(item => item.positionNum)
687
+                        }
688
+                    }
689
+                }
690
+            }).catch(() => { })
691
+        },
692
+        fetchSecurityTestData(params) {
693
+            securityTestItemClassification(params).then(res => {
694
+                if (res.code === 200 && res.data) {
695
+                    const data = res.data || []
696
+                    this.securityTestData.items = {
697
+                        labels: data.map(item => item.name),
698
+                        data: data.map(item => item.total)
699
+                    }
700
+                }
701
+            }).catch(() => { })
702
+            securityTestPassingStatus(params).then(res => {
703
+                if (res.code === 200 && res.data) {
704
+                    const data = res.data || []
705
+                    this.securityTestData.results = {
706
+                        labels: data.map(item => item.name),
707
+                        data: data.map(item => item.total)
708
+                    }
709
+                }
710
+            }).catch(() => { })
711
+            securityTestRegion(params).then(res => {
712
+                if (res.code === 200 && res.data) {
713
+                    const data = res.data || []
714
+                    this.securityTestData.areas = {
715
+                        labels: data.map(item => item.name),
716
+                        data: data.map(item => item.total)
717
+                    }
718
+                }
719
+            }).catch(() => { })
720
+        },
721
+        fetchSupervisionData(params) {
722
+            supervisionProblemPosition(params).then(res => {
723
+                if (res.code === 200 && res.data) {
724
+                    this.supervisionData = (res.data || []).map(item => ({
725
+                        num: item.total,
726
+                        name: item.name
727
+                    }))
728
+                }
729
+            }).catch(() => { })
730
+        },
731
+        fetchInterceptionData(params) {
732
+            realtimeInterceptionItem(params).then(res => {
733
+                if (res.code === 200 && res.data) {
734
+                    this.interceptionData = (res.data || []).map(item => ({
735
+                        num: item.total,
736
+                        name: item.name
737
+                    }))
738
+                }
739
+            }).catch(() => { })
740
+        }
741
+    }
742
+}
743
+</script>
744
+
745
+<style lang="scss" scoped>
746
+.dept-selector {
747
+    padding: 16rpx 32rpx;
748
+    background: rgba(30, 27, 75, 0.8);
749
+}
750
+
751
+.dept-select-trigger {
752
+    display: flex;
753
+    align-items: center;
754
+    justify-content: center;
755
+    gap: 12rpx;
756
+    padding: 16rpx 24rpx;
757
+    background: rgba(45, 42, 85, 0.8);
758
+    border: 1rpx solid rgba(167, 139, 250, 0.3);
759
+    border-radius: 50rpx;
760
+}
761
+
762
+.dept-select-trigger-disabled {
763
+    opacity: 0.5;
764
+    pointer-events: none;
765
+}
766
+
767
+.dept-name-text {
768
+    font-size: 26rpx;
769
+    color: rgba(255, 255, 255, 0.9);
770
+}
771
+
772
+.dept-profile-page {
773
+    min-height: 100vh;
774
+    background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
775
+    padding-bottom: 40rpx;
776
+}
777
+
778
+.page-header {
779
+    position: sticky;
780
+    top: 0;
781
+    z-index: 100;
782
+    background: rgba(30, 27, 75, 0.9);
783
+    backdrop-filter: blur(10px);
784
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
785
+}
786
+
787
+.header-title {
788
+    padding: 24rpx 32rpx;
789
+    display: flex;
790
+    justify-content: space-between;
791
+    align-items: center;
792
+
793
+    .title-main {
794
+        font-size: 36rpx;
795
+        font-weight: bold;
796
+        background: linear-gradient(90deg, #A78BFA, #60A5FA);
797
+        -webkit-background-clip: text;
798
+        -webkit-text-fill-color: transparent;
799
+        background-clip: text;
800
+    }
801
+}
802
+
803
+.header-right {
804
+    display: flex;
805
+    align-items: center;
806
+    gap: 16rpx;
807
+
808
+    .current-time {
809
+        font-size: 24rpx;
810
+        color: rgba(255, 255, 255, 0.6);
811
+    }
812
+}
813
+
814
+.time-filter {
815
+    padding: 16rpx 32rpx;
816
+    background: rgba(30, 27, 75, 0.8);
817
+}
818
+
819
+.time-scroll {
820
+    width: 100%;
821
+}
822
+
823
+.time-tags {
824
+    display: flex;
825
+    gap: 16rpx;
826
+    white-space: nowrap;
827
+}
828
+
829
+.time-tag {
830
+    padding: 8rpx 10rpx;
831
+    border-radius: 50rpx;
832
+    background: rgba(45, 42, 85, 0.8);
833
+    font-size: 24rpx;
834
+    color: rgba(255, 255, 255, 0.6);
835
+    transition: all 0.3s;
836
+
837
+    &.active {
838
+        background: #A78BFA;
839
+        color: #1E1B4B;
840
+        font-weight: 500;
841
+    }
842
+}
843
+
844
+.date-range-picker {
845
+    display: flex;
846
+    align-items: center;
847
+    gap: 16rpx;
848
+    margin-top: 16rpx;
849
+    padding-top: 16rpx;
850
+    border-top: 1rpx solid rgba(255, 255, 255, 0.1);
851
+}
852
+
853
+.date-input {
854
+    flex: 1;
855
+    padding: 12rpx 24rpx;
856
+    border-radius: 12rpx;
857
+    background: rgba(45, 42, 85, 0.8);
858
+    font-size: 24rpx;
859
+    color: rgba(255, 255, 255, 0.4);
860
+    text-align: center;
861
+
862
+    &.filled {
863
+        color: rgba(255, 255, 255, 0.9);
864
+    }
865
+}
866
+
867
+.date-separator {
868
+    font-size: 24rpx;
869
+    color: rgba(255, 255, 255, 0.5);
870
+    flex-shrink: 0;
871
+}
872
+
873
+.tab-nav {
874
+    padding: 16rpx 32rpx;
875
+    display: flex;
876
+    justify-content: center;
877
+    gap: 64rpx;
878
+}
879
+
880
+.tab-item {
881
+    font-size: 28rpx;
882
+    color: rgba(255, 255, 255, 0.5);
883
+    padding-bottom: 8rpx;
884
+    border-bottom: 2rpx solid transparent;
885
+    transition: all 0.3s;
886
+
887
+    &.active {
888
+        color: #A78BFA;
889
+        border-bottom-color: #A78BFA;
890
+    }
891
+}
892
+
893
+.page-content {
894
+    padding: 32rpx;
895
+    display: flex;
896
+    flex-direction: column;
897
+}
898
+</style>