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

Merge branch 'QAStatistic'

huoyi пре 3 недеља
родитељ
комит
1e7775d3aa

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

@@ -7,4 +7,40 @@ export function getDailyAllUsersRanking(params = {}) {
7 7
     method: 'get',
8 8
     params
9 9
   })
10
+}
11
+
12
+// 抽问抽答完成趋势
13
+export function getCompletionTrend(params = {}) {
14
+  return request({
15
+    url: '/v1/cs/app/daily-exam/completion-trend',
16
+    method: 'get',
17
+    params
18
+  })
19
+}
20
+
21
+// 错题分析 - 总体问题分布
22
+export function getWrongAnalysisOverview(params = {}) {
23
+  return request({
24
+    url: '/v1/cs/app/daily-exam/wrong-analysis/overview',
25
+    method: 'get',
26
+    params
27
+  })
28
+}
29
+
30
+// 错题分析 - 问题分布对比(雷达图)
31
+export function getWrongAnalysisRadar(params = {}) {
32
+  return request({
33
+    url: '/v1/cs/app/daily-exam/wrong-analysis/radar',
34
+    method: 'get',
35
+    params
36
+  })
37
+}
38
+
39
+// 错题分析 - 大类详情
40
+export function getWrongAnalysisCategoryDetail(params = {}) {
41
+  return request({
42
+    url: '/v1/cs/app/daily-exam/wrong-analysis/category-detail',
43
+    method: 'get',
44
+    params
45
+  })
10 46
 }

+ 380 - 0
src/pages/questionStatistics/components/CompletionTrend.vue

@@ -0,0 +1,380 @@
1
+<template>
2
+  <div class="completion-trend">
3
+    <!-- 时间筛选器 -->
4
+    <view class="time-filter-section">
5
+      <view class="filter-row">
6
+        <StaticsTab v-model="activeTimeRange" :tabs="timeRangeOptions" active-color="#2B7BFF"
7
+          @change="handleTimeRangeChange" />
8
+        <view v-if="activeTimeRange === 'custom'" class="custom-time-section">
9
+          <uni-datetime-picker v-model="dateRange" type="daterange" rangeSeparator="至" @change="handleDateRangeChange"
10
+            class="date-range-picker" :clear-icon="false"/>
11
+          <text class="days-count">共 {{ totalDays }} 天</text>
12
+        </view>
13
+      </view>
14
+    </view>
15
+
16
+    <!-- 图表标题 -->
17
+    <h3 class="chart-title">抽问抽答完成趋势图</h3>
18
+
19
+    <!-- 图表容器 -->
20
+    <div class="chart-container">
21
+      <div ref="trendChart" class="chart" style="width: 100%; height: 400rpx;"></div>
22
+    </div>
23
+
24
+    <!-- 加载状态 -->
25
+    <div v-if="loading" class="loading-container">
26
+      <text>加载中...</text>
27
+    </div>
28
+
29
+    <!-- 无数据提示 -->
30
+    <div v-if="!loading && chartData.length === 0" class="no-data">
31
+      <text>暂无趋势数据</text>
32
+    </div>
33
+  </div>
34
+</template>
35
+
36
+<script>
37
+import * as echarts from 'echarts';
38
+import StaticsTab from "@/pages/inspectionStatistics/components/StaticsTab.vue";
39
+import { getCompletionTrend } from '@/api/questionStatistics/questionStatistics';
40
+import { calculateTimeRange } from '@/utils/formatUtils.js';
41
+
42
+export default {
43
+  name: 'CompletionTrend',
44
+  components: {
45
+    StaticsTab
46
+  },
47
+  data() {
48
+    return {
49
+      loading: false,
50
+      activeTimeRange: 'month', // year, month, custom
51
+      customStartTime: '',
52
+      customEndTime: '',
53
+      dateRange: [],
54
+      totalDays: 0,
55
+      chartData: [],
56
+      xAxisData: [],
57
+      trendChart: null,
58
+      timeRangeOptions: [
59
+        { title: '本年', type: 'year' },
60
+        { title: '本月', type: 'month' },
61
+        { title: '自定义', type: 'custom' }
62
+      ]
63
+    };
64
+  },
65
+  mounted() {
66
+    this.setDefaultDates();
67
+    this.initChart();
68
+    this.loadData();
69
+  },
70
+  beforeDestroy() {
71
+    if (this.trendChart) {
72
+      this.trendChart.dispose();
73
+    }
74
+  },
75
+  methods: {
76
+    // 设置默认日期
77
+    setDefaultDates() {
78
+      const today = new Date();
79
+      // 本月第一天
80
+      const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
81
+
82
+      // 使用本地时间格式化,避免UTC时间转换问题
83
+      this.customStartTime = this.formatLocalDate(firstDayOfMonth);
84
+      this.customEndTime = this.formatLocalDate(today);
85
+      // 初始化dateRange
86
+      this.dateRange = [this.customStartTime, this.customEndTime];
87
+      this.calculateDays();
88
+    },
89
+    
90
+    // 格式化本地日期为YYYY-MM-DD格式
91
+    formatLocalDate(date) {
92
+      const year = date.getFullYear();
93
+      const month = String(date.getMonth() + 1).padStart(2, '0');
94
+      const day = String(date.getDate()).padStart(2, '0');
95
+      return `${year}-${month}-${day}`;
96
+    },
97
+    
98
+    initChart() {
99
+      const chartDom = this.$refs.trendChart;
100
+      if (chartDom) {
101
+        this.trendChart = echarts.init(chartDom);
102
+        this.setChartOption();
103
+      }
104
+    },
105
+    setChartOption() {
106
+      if (!this.trendChart) return;
107
+      console.log('setChartOption', this.chartData, this.xAxisData);
108
+      const option = {
109
+        // title: {
110
+        //   text: '抽问抽答完成趋势',
111
+        //   left: 'center',
112
+        //   textStyle: {
113
+        //     fontSize: 16,
114
+        //     fontWeight: 'normal'
115
+        //   }
116
+        // },
117
+        tooltip: {
118
+          trigger: 'axis',
119
+          formatter: function (params) {
120
+            let result = params[0].axisValue + '<br/>';
121
+            params.forEach(param => {
122
+              result += `${param.seriesName}: ${param.value}<br/>`;
123
+            });
124
+            return result;
125
+          }
126
+        },
127
+        legend: {
128
+          show: true,
129
+          // bottom: 10
130
+        },
131
+        grid: {
132
+          left: '3%',
133
+          right: '4%',
134
+          bottom: '15%',
135
+          containLabel: true
136
+        },
137
+        xAxis: {
138
+          type: 'category',
139
+          boundaryGap: false,
140
+          data: this.xAxisData
141
+        },
142
+        yAxis: {
143
+          type: 'value',
144
+          min: 0,
145
+          // max: 100,
146
+          // axisLabel: {
147
+          //   formatter: '{value}'
148
+          // }
149
+        },
150
+        series: this.chartData
151
+      };
152
+
153
+      this.trendChart.setOption(option);
154
+    },
155
+    handleTimeRangeChange(range) {
156
+      console.log('时间范围选择:', range, this.dateRange);
157
+      
158
+      // 使用calculateTimeRange计算时间范围
159
+      const timeRangeResult = calculateTimeRange(range, this.dateRange);
160
+      this.customStartTime = timeRangeResult.startDate;
161
+      this.customEndTime = timeRangeResult.endDate;
162
+      this.dateRange = [this.customStartTime, this.customEndTime];
163
+      
164
+      this.activeTimeRange = range;
165
+      this.calculateDays();
166
+      this.loadData();
167
+    },
168
+    handleDateRangeChange(e) {
169
+      if (e && e.length === 2) {
170
+        this.dateRange = e;
171
+        this.customStartTime = e[0];
172
+        this.customEndTime = e[1];
173
+        this.calculateDays();
174
+        this.loadData();
175
+      }
176
+    },
177
+    
178
+    // 计算天数
179
+    calculateDays() {
180
+      if (this.customStartTime && this.customEndTime) {
181
+        const start = new Date(this.customStartTime);
182
+        const end = new Date(this.customEndTime);
183
+        const diffTime = Math.abs(end - start);
184
+        this.totalDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
185
+      }
186
+    },
187
+    
188
+    async loadData() {
189
+      this.loading = true;
190
+
191
+      try {
192
+        // 构建请求参数
193
+        const params = {
194
+          timeType: this.activeTimeRange
195
+        };
196
+
197
+        // 如果是自定义时间范围,添加开始和结束日期
198
+        if (this.activeTimeRange === 'custom') {
199
+          if (!this.customStartTime || !this.customEndTime) {
200
+            uni.showToast({
201
+              title: '请选择完整的时间范围',
202
+              icon: 'none'
203
+            });
204
+            this.loading = false;
205
+            return;
206
+          }
207
+          params.startDate = this.customStartTime;
208
+          params.endDate = this.customEndTime;
209
+        }
210
+
211
+        // 调用API接口
212
+        const response = await getCompletionTrend(params);
213
+
214
+        // 检查权限控制
215
+        if (response.show === false) {
216
+          uni.showToast({
217
+            title: '当前角色无权限查看此数据',
218
+            icon: 'none'
219
+          });
220
+          this.chartData = [];
221
+          this.setChartOption();
222
+          return;
223
+        }
224
+
225
+        // 处理API返回的数据
226
+        const { processedData, xAxisData } = this.processApiData(response);
227
+        this.chartData = processedData;
228
+        this.xAxisData = xAxisData;
229
+        this.setChartOption();
230
+
231
+      } catch (error) {
232
+        console.error('加载数据失败:', error);
233
+        uni.showToast({
234
+          title: '加载数据失败',
235
+          icon: 'none'
236
+        });
237
+      } finally {
238
+        this.loading = false;
239
+      }
240
+    },
241
+
242
+    // 处理API返回的数据格式
243
+    processApiData(apiData) {
244
+      console.log('processApiData', apiData);
245
+
246
+      // 根据API返回的数据结构进行处理
247
+      if (!apiData || !apiData.model) {
248
+        return { processedData: [], xAxisData: [] };
249
+      }
250
+
251
+      const { xAxis, series } = apiData.model;
252
+
253
+      // 处理x轴数据
254
+      const xAxisData = xAxis || [];
255
+
256
+      // 处理系列数据
257
+      const processedData = [];
258
+
259
+      if (series && series.length > 0) {
260
+        // 如果是站长角色,显示多条线;如果是科长角色,显示单条线
261
+        // 这里简化处理,只显示第一条线
262
+        for (let i = 0; i < series.length; i++) {
263
+          processedData.push({
264
+            name: series[i].name,
265
+            type: 'line',
266
+            data: series[i].data
267
+          });
268
+        }
269
+
270
+      }
271
+
272
+      return { processedData, xAxisData };
273
+    },
274
+
275
+
276
+  },
277
+  watch: {
278
+    chartData: {
279
+      handler() {
280
+        this.$nextTick(() => {
281
+          this.setChartOption();
282
+        });
283
+      },
284
+      deep: true
285
+    }
286
+  }
287
+};
288
+</script>
289
+
290
+<style lang="scss" scoped>
291
+.completion-trend {
292
+  padding: 10rpx 0;
293
+
294
+  .time-filter-section {
295
+    margin-bottom: 20rpx;
296
+    padding: 0 20rpx;
297
+  }
298
+
299
+  .filter-row {
300
+    display: flex;
301
+    align-items: center;
302
+    flex-direction: row;
303
+
304
+    .custom-time-section {
305
+        position: relative;
306
+        display: flex;
307
+        align-items: center;
308
+        gap: 24rpx;
309
+        margin-top: 2rpx;
310
+        width: 350rpx;
311
+
312
+        .date-range-picker {
313
+          ::v-deep .icon-calendar {
314
+            display: none !important;
315
+          }
316
+
317
+          ::v-deep .uni-date-range {
318
+            font-size: 16rpx !important;
319
+            background: transparent !important;
320
+            color: black !important;
321
+          }
322
+
323
+          ::v-deep .uni-date-x {
324
+            height: 25rpx !important;
325
+            line-height: 25rpx !important;
326
+
327
+            .range-separator {
328
+              font-size: 20rpx !important;
329
+              padding: 0 !important;
330
+            }
331
+
332
+            .uni-date__x-input {
333
+              height: 25rpx !important;
334
+              line-height: 25rpx !important;
335
+            }
336
+          }
337
+
338
+          ::v-deep .uni-date-x--border {
339
+            border: none !important;
340
+          }
341
+        }
342
+
343
+        .days-count {
344
+          position: absolute;
345
+          right: -86rpx;
346
+          top: -8rpx;
347
+          font-size: 25rpx;
348
+          color: black;
349
+          font-weight: 500;
350
+          white-space: nowrap;
351
+        }
352
+      }
353
+  }
354
+
355
+  .chart-title {
356
+    font-size: 32rpx;
357
+    font-weight: 600;
358
+    color: #333;
359
+    margin-bottom: 24rpx;
360
+    text-align: center;
361
+  }
362
+
363
+  .chart-container {
364
+    background: #fff;
365
+    border-radius: 12rpx;
366
+    padding: 24rpx;
367
+    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
368
+  }
369
+
370
+  .loading-container,
371
+  .no-data {
372
+    display: flex;
373
+    justify-content: center;
374
+    align-items: center;
375
+    height: 200rpx;
376
+    font-size: 28rpx;
377
+    color: #999;
378
+  }
379
+}
380
+</style>

Разлика између датотеке није приказан због своје велике величине
+ 1062 - 0
src/pages/questionStatistics/components/ErrorAnalysis.vue


+ 195 - 0
src/pages/questionStatistics/components/RealTimeStatus.vue

@@ -0,0 +1,195 @@
1
+<template>
2
+  <div class="real-time-status">
3
+    <!-- 加载状态 -->
4
+    <div v-if="loading" class="loading-container">
5
+      <text>加载中...</text>
6
+    </div>
7
+
8
+    <!-- 统计表格区域 - 遍历分组后的数据 -->
9
+    <div v-else class="statistics-section">
10
+      <div v-for="(group, index) in groupedData" :key="index" class="statistic-table-wrapper">
11
+        <!-- 每个统计表格的标题 - 使用deptName -->
12
+        <h2 class="table-title">{{ group.title }}</h2>
13
+
14
+        <!-- statistic-table组件 -->
15
+        <statistic-table :columns="tableColumns" :data="group.data">
16
+          <template #column-progress="{ row, index }">
17
+            <view class="progress-container">
18
+              <u-line-progress :percentage="Math.round(row.progress * 100)" :showText="false"
19
+                :activeColor="getProgressColor(row.progress * 100)" height="20rpx"
20
+                width="200rpx"></u-line-progress>
21
+              <text class="progress-text">{{ Math.round(row.progress * 100) }}%</text>
22
+            </view>
23
+          </template>
24
+        </statistic-table>
25
+      </div>
26
+
27
+      <!-- 无数据提示 -->
28
+      <div v-if="groupedData.length === 0" class="no-data">
29
+        <text>暂无统计数据</text>
30
+      </div>
31
+    </div>
32
+  </div>
33
+</template>
34
+
35
+<script>
36
+import StatisticTable from "@/components/statistic-table/statistic-table.vue";
37
+import { getDailyAllUsersRanking } from "@/api/questionStatistics/questionStatistics";
38
+
39
+export default {
40
+  name: 'RealTimeStatus',
41
+  components: {
42
+    StatisticTable
43
+  },
44
+  props: {
45
+    loading: {
46
+      type: Boolean,
47
+      default: false
48
+    }
49
+  },
50
+  data() {
51
+    return {
52
+      // 固定的表格列配置
53
+      tableColumns: [
54
+        { props: 'userName', title: '姓名' },
55
+        { props: 'completedTasks', title: '答题进度' },
56
+        { props: 'avgScore', title: '平均分' },
57
+        { props: 'progress', title: '完成率', slot: true }
58
+      ],
59
+      // 分组后的数据
60
+      groupedData: [],
61
+      // 原始接口数据
62
+      apiData: []
63
+    };
64
+  },
65
+  mounted() {
66
+    this.getDailyAllUsersRanking();
67
+  },
68
+  methods: {
69
+    async getDailyAllUsersRanking() {
70
+      this.$emit('update:loading', true);
71
+      
72
+      try {
73
+        const res = await getDailyAllUsersRanking({
74
+          pageNum: 1,
75
+          pageSize: 100
76
+        });
77
+        
78
+        if (res.code === 200) {
79
+          this.apiData = res.data.rows || [];
80
+          this.groupDataByDept();
81
+        } else {
82
+          uni.showToast({
83
+            title: '获取数据失败',
84
+            icon: 'none'
85
+          });
86
+        }
87
+      } catch (error) {
88
+        console.error('获取数据失败:', error);
89
+        uni.showToast({
90
+          title: '获取数据失败',
91
+          icon: 'none'
92
+        });
93
+      } finally {
94
+        this.$emit('update:loading', false);
95
+      }
96
+    },
97
+    // 按照deptName分组数据
98
+    groupDataByDept() {
99
+      if (!this.apiData || this.apiData.length === 0) {
100
+        this.groupedData = [];
101
+        return;
102
+      }
103
+
104
+      // 使用Map来分组数据
105
+      const deptMap = new Map();
106
+
107
+      this.apiData.forEach(item => {
108
+        const deptName = item.deptName || '未知部门';
109
+
110
+        if (!deptMap.has(deptName)) {
111
+          deptMap.set(deptName, []);
112
+        }
113
+
114
+        // 格式化数据,确保包含所有需要的字段
115
+        const formattedItem = {
116
+          userName: item.userName || '未知用户',
117
+          completedTasks: item.completedTasks,
118
+          avgScore: item.avgScore || 0,
119
+          progress: item.completedTasks == 0 ? 0 : item.completedTasks / item.totalTasks
120
+        };
121
+
122
+        deptMap.get(deptName).push(formattedItem);
123
+      });
124
+
125
+      // 转换为数组格式
126
+      this.groupedData = Array.from(deptMap.entries()).map(([deptName, data]) => ({
127
+        title: deptName,
128
+        data: data
129
+      }));
130
+
131
+      console.log('分组后的数据:', this.groupedData);
132
+    },
133
+    // 根据完成比例返回对应的颜色
134
+    getProgressColor(percentage) {
135
+      if (percentage >= 100) {
136
+        return '#2B7BFF'; // 大于等于100%时使用蓝色
137
+      } else if (percentage > 0 && percentage < 100) {
138
+        return '#03AF43'; // 0-100%之间使用绿色
139
+      } else {
140
+        return '#E40808'; // 0%时使用红色
141
+      }
142
+    }
143
+  }
144
+};
145
+</script>
146
+
147
+<style lang="scss" scoped>
148
+.real-time-status {
149
+  margin-top: 20rpx;
150
+  .loading-container {
151
+    display: flex;
152
+    justify-content: center;
153
+    align-items: center;
154
+    height: 200rpx;
155
+    font-size: 28rpx;
156
+    color: #999;
157
+  }
158
+
159
+  .statistics-section {
160
+    .statistic-table-wrapper {
161
+      margin-bottom: 32rpx;
162
+      
163
+      .table-title {
164
+        font-size: 32rpx;
165
+        font-weight: 600;
166
+        color: #333;
167
+        margin-bottom: 24rpx;
168
+        padding-left: 16rpx;
169
+        border-left: 8rpx solid #2B7BFF;
170
+      }
171
+    }
172
+
173
+    .progress-container {
174
+      display: flex;
175
+      align-items: center;
176
+      gap: 16rpx;
177
+
178
+      .progress-text {
179
+        font-size: 24rpx;
180
+        color: #666;
181
+        min-width: 60rpx;
182
+      }
183
+    }
184
+
185
+    .no-data {
186
+      display: flex;
187
+      justify-content: center;
188
+      align-items: center;
189
+      height: 200rpx;
190
+      font-size: 28rpx;
191
+      color: #999;
192
+    }
193
+  }
194
+}
195
+</style>

+ 119 - 135
src/pages/questionStatistics/index.vue

@@ -1,37 +1,30 @@
1 1
 <template>
2
-    <home-container>
2
+    <home-container :customStyle="{ background: 'none' }">
3 3
         <div class="question-statistics-container">
4
-
5
-
6
-
7
-            <!-- 加载状态 -->
8
-            <div v-if="loading" class="loading-container">
9
-                <text>加载中...</text>
4
+            <!-- 选项卡 -->
5
+            <div class="tab-container">
6
+                <h-tabs class="h-tab" v-model="activeTab" :tabs="tabs" :activeColor="'#2B7BFF'" value-key="value" label-key="label"
7
+                    @change="handleTabChange" />
10 8
             </div>
11 9
 
12
-            <!-- 统计表格区域 - 遍历分组后的数据 -->
13
-            <div v-else class="statistics-section">
14
-                <div v-for="(group, index) in groupedData" :key="index" class="statistic-table-wrapper">
15
-                    <!-- 每个统计表格的标题 - 使用deptName -->
16
-                    <h2 class="table-title">{{ group.title }}</h2>
17
-
18
-                    <!-- statistic-table组件 -->
19
-                    <statistic-table :columns="tableColumns" :data="group.data" >
20
-                        <template #column-progress="{ row, index }">
21
-                            <view class="progress-container">
22
-                                <u-line-progress :percentage="Math.round(row.progress * 100)" :showText="false"
23
-                                    :activeColor="getProgressColor(row.progress * 100)" height="20rpx"
24
-                                    width="200rpx"></u-line-progress>
25
-                                <text class="progress-text">{{ Math.round(row.progress * 100) }}%</text>
26
-                            </view>
27
-                        </template>
28
-                    </statistic-table>
29
-                </div>
30
-
31
-                <!-- 无数据提示 -->
32
-                <div v-if="groupedData.length === 0" class="no-data">
33
-                    <text>暂无统计数据</text>
34
-                </div>
10
+            <!-- 组件切换区域 -->
11
+            <div class="component-container">
12
+                <!-- 实时状态组件 -->
13
+                <real-time-status 
14
+                    v-if="activeTab === 'realtime'" 
15
+                    :loading="loading"
16
+                    @update:loading="loading = $event"
17
+                />
18
+                
19
+                <!-- 完成趋势组件 -->
20
+                <completion-trend 
21
+                    v-else-if="activeTab === 'trend'" 
22
+                />
23
+                
24
+                <!-- 错题分析组件 -->
25
+                <error-analysis 
26
+                    v-else-if="activeTab === 'analysis'" 
27
+                />
35 28
             </div>
36 29
         </div>
37 30
     </home-container>
@@ -39,97 +32,56 @@
39 32
 
40 33
 <script>
41 34
 import HomeContainer from "@/components/HomeContainer.vue";
42
-import StatisticTable from "@/components/statistic-table/statistic-table.vue";
43
-import { getDailyAllUsersRanking } from "@/api/questionStatistics/questionStatistics";
35
+import RealTimeStatus from "./components/RealTimeStatus.vue";
36
+import CompletionTrend from "./components/CompletionTrend.vue";
37
+import ErrorAnalysis from "./components/ErrorAnalysis.vue";
38
+import HTabs from "@/components/h-tabs/h-tabs.vue";
39
+import { mapState } from 'vuex'
44 40
 
45 41
 export default {
46 42
     name: 'QuestionStatistics',
47 43
     components: {
48 44
         HomeContainer,
49
-        StatisticTable
45
+        RealTimeStatus,
46
+        CompletionTrend,
47
+        ErrorAnalysis,
48
+        HTabs
50 49
     },
51 50
     data() {
52 51
         return {
53 52
             loading: false,
54
-            // 固定的表格列配置
55
-            tableColumns: [
56
-                { props: 'userName', title: '姓名' },
57
-                { props: 'completedTasks', title: '答题进度' },
58
-                { props: 'avgScore', title: '平均分' },
59
-                { props: 'progress', title: '完成率', slot: true }
60
-            ],
61
-            // 分组后的数据
62
-            groupedData: [],
63
-            // 原始接口数据
64
-            apiData: []
53
+            activeTab: 'realtime', // realtime, trend, analysis
54
+            allTabs: [
55
+                { label: '实时状态', value: 'realtime', style: { 'color': '#666666' }, activeStyle: { 'color': '#2B7BFF' } },
56
+                { label: '完成趋势', value: 'trend', style: { 'color': '#666666' }, activeStyle: { 'color': '#2B7BFF' } },
57
+                { label: '错题分析', value: 'analysis', style: { 'color': '#666666' }, activeStyle: { 'color': '#2B7BFF' } }
58
+            ]
65 59
         };
66 60
     },
67
-    mounted() {
68
-        this.getDailyAllUsersRanking();
69
-    },
70
-    methods: {
71
-        async getDailyAllUsersRanking() {
72
-            const res = await getDailyAllUsersRanking({
73
-                pageNum: 1,
74
-                pageSize: 100
75
-            });
76
-            console.log('getDailyAllUsersRanking', res);
77
-            if (res.code === 200) {
78
-                this.apiData = res.data.rows || [];
79
-
80
-                this.groupDataByDept();
81
-            } else {
82
-                uni.showToast({
83
-                    title: '获取数据失败',
84
-                    icon: 'none'
85
-                });
61
+    computed: {
62
+        ...mapState({
63
+            userRoles: state => state.user.roles
64
+        }),
65
+        // 根据用户角色过滤tabs
66
+        tabs() {
67
+            // 如果是班组长,过滤掉完成趋势tab
68
+            if (this.isBanZuZhang) {
69
+                return this.allTabs.filter(tab => tab.value !== 'trend');
86 70
             }
71
+            return this.allTabs;
87 72
         },
88
-        // 按照deptName分组数据
89
-        groupDataByDept() {
90
-            if (!this.apiData || this.apiData.length === 0) {
91
-                this.groupedData = [];
92
-                return;
93
-            }
94
-
95
-            // 使用Map来分组数据
96
-            const deptMap = new Map();
97
-
98
-            this.apiData.forEach(item => {
99
-                const deptName = item.deptName || '未知部门';
100
-
101
-                if (!deptMap.has(deptName)) {
102
-                    deptMap.set(deptName, []);
103
-                }
104
-
105
-                // 格式化数据,确保包含所有需要的字段
106
-                const formattedItem = {
107
-                    userName: item.userName || '未知用户',
108
-                    completedTasks: item.completedTasks,
109
-                    avgScore: item.avgScore || 0,
110
-                    progress: item.completedTasks == 0 ? 0 : item.completedTasks / item.totalTasks
111
-                };
112
-
113
-                deptMap.get(deptName).push(formattedItem);
114
-            });
115
-
116
-            // 转换为数组格式
117
-            this.groupedData = Array.from(deptMap.entries()).map(([deptName, data]) => ({
118
-                title: deptName,
119
-                data: data
120
-            }));
121
-
122
-            console.log('分组后的数据:', this.groupedData);
123
-        },
124
-        // 根据完成比例返回对应的颜色
125
-        getProgressColor(percentage) {
126
-            if (percentage >= 100) {
127
-                return '#2B7BFF'; // 大于等于100%时使用蓝色
128
-            } else if (percentage > 0 && percentage < 100) {
129
-                return '#03AF43'; // 0-100%之间使用绿色
130
-            } else {
131
-                return '#E40808'; // 0%时使用红色
132
-            }
73
+        // 判断是否为班组长
74
+        isBanZuZhang() {
75
+            let roles = this.userRoles;
76
+            console.log('用户角色:', roles);
77
+            return roles && (roles.includes('banzuzhang') || roles.includes('teamLeader'));
78
+        }
79
+    },
80
+    methods: {
81
+        // 选项卡切换事件
82
+        handleTabChange(index) {
83
+            this.activeTab = index;
84
+            console.log('切换到选项卡:', this.tabs.find(tab => tab.value === index)?.label);
133 85
         }
134 86
     }
135 87
 };
@@ -137,43 +89,75 @@ export default {
137 89
 
138 90
 <style lang="scss" scoped>
139 91
 .question-statistics-container {
140
-    padding: 32rpx;
92
+    position: relative;
93
+    padding-top: 80rpx;
141 94
 }
142 95
 
96
+.tab-container {
97
+    position: fixed;
98
+    top: 80rpx;
99
+    left: 0;
100
+    right: 0;
101
+    z-index: 1000;
102
+    padding: 15px 15px 0 15px;
103
+    background-color: white;
104
+    .h-tab{
105
+        padding-bottom: 10px !important;
106
+    }
107
+}
143 108
 
109
+.capsule-tab-container {
110
+    display: inline-flex;
111
+    
112
+    // border: 2rpx solid #2B7BFF;
113
+    border-radius: 32rpx;
114
+    padding: 2rpx;
115
+    box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
116
+    
117
+}
144 118
 
145
-.loading-container {
146
-    display: flex;
147
-    justify-content: center;
148
-    align-items: center;
149
-    height: 200rpx;
150
-    font-size: 32rpx;
151
-    color: #666;
119
+.capsule-tab-item {
120
+    position: relative;
121
+    padding: 8rpx 24rpx;
122
+    cursor: pointer;
123
+    transition: all 0.3s ease;
124
+    user-select: none;
125
+
126
+    // 非激活状态 - 蓝字白底蓝边框
127
+    &:not(.capsule-tab-active) {
128
+        background: #ffffff;
129
+        border: 1rpx solid #2B7BFF;
130
+
131
+        .capsule-tab-text {
132
+            color: #2B7BFF;
133
+            font-weight: normal;
134
+        }
135
+    }
136
+
137
+    // 激活状态 - 白字蓝底
138
+    &.capsule-tab-active {
139
+        background: #2B7BFF;
140
+        border: 1rpx solid #2B7BFF;
141
+        box-shadow: 0 2rpx 8rpx rgba(43, 123, 255, 0.3);
142
+
143
+        .capsule-tab-text {
144
+            color: #ffffff;
145
+            font-weight: bold;
146
+        }
147
+    }
152 148
 }
153 149
 
154
-.statistics-section {
155
-    display: flex;
156
-    flex-direction: column;
157
-    gap: 48rpx;
150
+.capsule-tab-text {
151
+    font-size: 24rpx;
152
+    line-height: 1;
153
+    transition: all 0.3s ease;
158 154
 }
159 155
 
160
-.statistic-table-wrapper {
161
-    background: #fff;
162
-    border-radius: 16rpx;
163
-    padding: 32rpx;
164
-    box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
165
-
166
-    .table-title {
167
-        font-size: 36rpx;
168
-        font-weight: 600;
169
-        color: #333;
170
-        margin-bottom: 24rpx;
171
-        padding-bottom: 16rpx;
172
-        border-bottom: 2rpx solid #f0f0f0;
173
-    }
156
+.component-container {
157
+    min-height: 400rpx;
174 158
 }
175 159
 
176
-.no-data {
160
+.loading-container {
177 161
     display: flex;
178 162
     justify-content: center;
179 163
     align-items: center;