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

feat: 员工画像页面接入真实数据

替换 employeeProfile/index.vue 中的 Mock 数据为 API 调用,新增员工
姓名自动补全搜索,支持时间范围切换,雷达图/维度明细/获奖记录/考试
成绩均由后端接口驱动;score/index.js 补充 searchPortraitUsers 和
getEmployeePortrait 两个函数。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
simonlll пре 1 месец
родитељ
комит
7ecf4aa06e
2 измењених фајлова са 330 додато и 236 уклоњено
  1. 8 0
      src/api/score/index.js
  2. 322 236
      src/views/portraitManagement/employeeProfile/index.vue

+ 8 - 0
src/api/score/index.js

@@ -87,6 +87,14 @@ export function syncLedgerByType(type) {
87 87
   return request({ url: `/ledger/sync/${type}`, method: 'post' })
88 88
 }
89 89
 
90
+// ===== 员工画像 =====
91
+export function searchPortraitUsers(keyword) {
92
+  return request({ url: '/score/portrait/searchUsers', method: 'get', params: { keyword } })
93
+}
94
+export function getEmployeePortrait(params) {
95
+  return request({ url: '/score/portrait/employee', method: 'get', params })
96
+}
97
+
90 98
 // ===== 推送配置 =====
91 99
 export function listPushConfig(params) {
92 100
   return request({ url: '/score/pushConfig/list', method: 'get', params })

+ 322 - 236
src/views/portraitManagement/employeeProfile/index.vue

@@ -7,8 +7,9 @@
7 7
         <el-button size="small" @click="selectTime('month')" :type="currentTime === 'month' ? 'primary' : 'default'">近一月</el-button>
8 8
         <el-button size="small" @click="selectTime('quarter')" :type="currentTime === 'quarter' ? 'primary' : 'default'">近三月</el-button>
9 9
         <el-button size="small" @click="selectTime('year')" :type="currentTime === 'year' ? 'primary' : 'default'">近一年</el-button>
10
-        <el-button size="small" type="default">自定义时间范围</el-button>
10
+        <el-button size="small" @click="selectTime('custom')" :type="currentTime === 'custom' ? 'primary' : 'default'">自定义</el-button>
11 11
         <el-date-picker
12
+          v-if="currentTime === 'custom'"
12 13
           v-model="dateRange"
13 14
           type="daterange"
14 15
           range-separator="至"
@@ -16,51 +17,68 @@
16 17
           end-placeholder="结束时间"
17 18
           size="small"
18 19
           style="margin-left: 10px;"
20
+          @change="loadPortrait"
19 21
         />
20 22
       </div>
21
-      <div class="org-selector">
22
-        <el-select v-model="selectedOrg" placeholder="请选择组织" size="small" style="width: 200px;">
23
-          <el-option label="旅检一科" value="dept1" />
24
-          <el-option label="旅检二科" value="dept2" />
25
-          <el-option label="旅检三科" value="dept3" />
26
-        </el-select>
23
+      <div class="person-selector">
24
+        <el-autocomplete
25
+          v-model="personName"
26
+          :fetch-suggestions="queryUsers"
27
+          placeholder="请搜索员工姓名"
28
+          size="small"
29
+          style="width: 240px;"
30
+          @select="handlePersonSelect"
31
+          @clear="portrait = null"
32
+          clearable
33
+        >
34
+          <template #suffix>
35
+            <el-icon><Search /></el-icon>
36
+          </template>
37
+          <template #default="{ item }">
38
+            <span>{{ item.nickName }}</span>
39
+            <span v-if="item.deptName" style="font-size:12px;color:#999;margin-left:8px">{{ item.deptName }}</span>
40
+          </template>
41
+        </el-autocomplete>
27 42
       </div>
28 43
     </div>
29 44
 
30
-    <div class="main-content">
45
+    <div v-if="!portrait" class="empty-tip">
46
+      <el-empty description="请在右上角搜索员工姓名以查看画像" />
47
+    </div>
48
+
49
+    <div v-else v-loading="loading" class="main-content">
50
+      <!-- 第一行:基本信息 + 综合评分 + 补充信息 -->
31 51
       <div class="row">
32 52
         <div class="card basic-info-card">
33 53
           <div class="avatar-section">
34
-            <img class="avatar" src="https://trae-api-cn.mchost.guru/api/ide/v1/text_to_image?prompt=portrait%20of%20asian%20male%20in%20business%20suit&image_size=square" alt="头像" />
54
+            <div v-if="portrait.avatar" class="avatar-img-wrap">
55
+              <img class="avatar" :src="portrait.avatar" alt="头像" />
56
+            </div>
57
+            <div v-else class="avatar-placeholder">{{ portrait.personName ? portrait.personName.charAt(0) : '?' }}</div>
35 58
           </div>
36 59
           <div class="info-section">
37
-            <h2 class="name">陈雨桐</h2>
60
+            <h2 class="name">{{ portrait.personName }}</h2>
38 61
             <div class="info-grid">
39 62
               <div class="info-item">
40
-                <span class="label">所属部门及队:</span>
41
-                <span class="value">旅检一科/雨桐班组/鹏飞小组</span>
42
-              </div>
43
-              <div class="info-item">
44
-                <span class="label">出生日期:</span>
45
-                <span class="value">1990年10月18日</span>
63
+                <span class="label">所属部门:</span>
64
+                <span class="value">{{ portrait.deptPath || '-' }}</span>
46 65
               </div>
47 66
               <div class="info-item">
48 67
                 <span class="label">技能等级:</span>
49
-                <span class="value">二级</span>
68
+                <span class="value">{{ portrait.qualificationLevelText || '-' }}</span>
50 69
               </div>
51 70
               <div class="info-item">
52 71
                 <span class="label">学历:</span>
53
-                <span class="value">本科</span>
72
+                <span class="value">{{ portrait.schooling || '-' }}</span>
54 73
               </div>
55 74
               <div class="info-item">
56
-                <span class="label">专业:</span>
57
-                <span class="value">物流运输</span>
75
+                <span class="label">安检岗位:</span>
76
+                <span class="value">{{ portrait.securityInspectionPosition || '-' }}</span>
77
+              </div>
78
+              <div class="info-item" v-if="portrait.phonenumber">
79
+                <span class="label">联系电话:</span>
80
+                <span class="value">{{ portrait.phonenumber }}</span>
58 81
               </div>
59
-            </div>
60
-            <div class="tags">
61
-              <span class="label">标签:</span>
62
-              <el-tag size="small" type="primary">通道组长</el-tag>
63
-              <el-tag size="small" type="warning">旅检</el-tag>
64 82
             </div>
65 83
           </div>
66 84
         </div>
@@ -68,31 +86,41 @@
68 86
         <div class="card score-card">
69 87
           <h3 class="card-title">综合评价</h3>
70 88
           <div class="score-display">
71
-            <span class="score">78<span class="plus">+2</span></span>
89
+            <span class="score" :style="scoreColor">{{ portrait.totalScore }}</span>
90
+            <div class="score-level" :style="scoreColor">{{ scoreLevel }}</div>
72 91
           </div>
73 92
         </div>
74 93
 
75 94
         <div class="card extra-info-card">
76 95
           <h3 class="card-title">补充信息</h3>
77 96
           <div class="extra-info-list">
78
-            <div class="extra-item"><span class="label">政治面貌:</span><span class="value">党员</span></div>
79
-            <div class="extra-item"><span class="label">性别:</span><span class="value">男</span></div>
80
-            <div class="extra-item"><span class="label">籍贯:</span><span class="value">重庆市</span></div>
81
-            <div class="extra-item"><span class="label">民族:</span><span class="value">汉族</span></div>
82
-            <div class="extra-item"><span class="label">年龄:</span><span class="value">28岁</span></div>
83
-            <div class="extra-item"><span class="label">司龄:</span><span class="value">6年</span></div>
84
-            <div class="extra-item"><span class="label">性格特征:</span><span class="value">检查员型</span></div>
85
-            <div class="extra-item"><span class="label">工作风格:</span><span class="value">尽责型</span></div>
97
+            <div class="extra-item"><span class="label">政治面貌:</span><span class="value">{{ portrait.politicalStatusText || '-' }}</span></div>
98
+            <div class="extra-item"><span class="label">性别:</span><span class="value">{{ portrait.sexText || '-' }}</span></div>
99
+            <div class="extra-item"><span class="label">司龄:</span><span class="value">{{ portrait.workYears != null ? portrait.workYears + '年' : '-' }}</span></div>
100
+            <div class="extra-item"><span class="label">开机年限:</span><span class="value">{{ portrait.securityCheckYears != null ? portrait.securityCheckYears + '年' : '-' }}</span></div>
101
+            <div class="extra-item"><span class="label">性格特征:</span><span class="value">{{ portrait.characterCharacteristics || '-' }}</span></div>
102
+            <div class="extra-item"><span class="label">工作风格:</span><span class="value">{{ portrait.workingStyle || '-' }}</span></div>
86 103
           </div>
87 104
         </div>
88 105
       </div>
89 106
 
107
+      <!-- 第二行:工作履历 + 雷达图 + 获奖记录 -->
90 108
       <div class="row">
91 109
         <div class="card work-experience-card">
92 110
           <h3 class="card-title">工作履历</h3>
93 111
           <div class="experience-list">
94
-            <div class="experience-item">2020.11入职/司龄6年/开机年限5年/现任班组长</div>
95
-            <div class="experience-item">2020.11入职/司龄6年/开机年限5年/现任班组长</div>
112
+            <div class="experience-item" v-if="portrait.startWorkingDate">
113
+              入职时间:{{ portrait.startWorkingDate }}
114
+              <span v-if="portrait.workYears != null"> / 司龄 {{ portrait.workYears }} 年</span>
115
+            </div>
116
+            <div class="experience-item" v-if="portrait.securityCheckStartDate">
117
+              开机时间:{{ portrait.securityCheckStartDate }}
118
+              <span v-if="portrait.securityCheckYears != null"> / 开机年限 {{ portrait.securityCheckYears }} 年</span>
119
+            </div>
120
+            <div class="experience-item" v-if="portrait.securityInspectionPosition">
121
+              现任岗位:{{ portrait.securityInspectionPosition }}
122
+            </div>
123
+            <div v-if="!portrait.startWorkingDate && !portrait.securityCheckStartDate" class="empty-text">暂无履历数据</div>
96 124
           </div>
97 125
         </div>
98 126
 
@@ -103,58 +131,81 @@
103 131
 
104 132
         <div class="card award-card">
105 133
           <h3 class="card-title">获奖记录</h3>
106
-          <table class="award-table">
134
+          <table class="award-table" v-if="portrait.awards && portrait.awards.length">
107 135
             <thead>
108 136
               <tr>
109
-                <th>站级</th>
110
-                <th>个人荣誉</th>
111
-                <th>航站楼加分</th>
112
-                <th>8分</th>
137
+                <th>类型</th>
138
+                <th>内容</th>
139
+                <th>分</th>
140
+                <th>日期</th>
113 141
               </tr>
114 142
             </thead>
115 143
             <tbody>
116
-              <tr>
117
-                <td>站级</td>
118
-                <td>个人荣誉</td>
119
-                <td>小额奖励</td>
120
-                <td>100元</td>
121
-              </tr>
122
-              <tr>
123
-                <td>部门级</td>
124
-                <td>个人荣誉</td>
125
-                <td>小额奖励</td>
126
-                <td>100元</td>
127
-              </tr>
128
-              <tr>
129
-                <td>部门级</td>
130
-                <td>个人荣誉</td>
131
-                <td>小额奖励</td>
132
-                <td>100元</td>
144
+              <tr v-for="(award, idx) in portrait.awards" :key="idx">
145
+                <td>{{ award.type }}</td>
146
+                <td>{{ award.content }}</td>
147
+                <td style="color:#67c23a;font-weight:500">+{{ award.score }}</td>
148
+                <td>{{ award.date }}</td>
133 149
               </tr>
134 150
             </tbody>
135 151
           </table>
152
+          <div v-else class="empty-text">暂无获奖记录</div>
136 153
         </div>
137 154
       </div>
138 155
 
156
+      <!-- 第三行:维度明细 + 培训情况 -->
139 157
       <div class="row">
140
-        <div class="card position-card">
141
-          <h3 class="card-title">业务岗位</h3>
142
-          <div class="position-list">
143
-            <div class="position-item">证件检查岗位</div>
144
-            <div class="position-item">防爆检查岗位</div>
145
-            <div class="position-item">人身检查岗位</div>
146
-            <div class="position-item">开包检查岗位</div>
147
-            <div class="position-item">X射线安检仪操作岗位</div>
148
-          </div>
158
+        <div class="card dim-detail-card">
159
+          <h3 class="card-title">维度得分明细</h3>
160
+          <table class="award-table" v-if="portrait.dimensions && portrait.dimensions.length">
161
+            <thead>
162
+              <tr>
163
+                <th>维度</th>
164
+                <th>基础分</th>
165
+                <th>事项分</th>
166
+                <th>得分</th>
167
+                <th>权重</th>
168
+                <th>贡献分</th>
169
+              </tr>
170
+            </thead>
171
+            <tbody>
172
+              <tr v-for="(dim, idx) in portrait.dimensions" :key="idx">
173
+                <td>{{ dim.name }}</td>
174
+                <td>{{ dim.baseScore }}</td>
175
+                <td :style="Number(dim.eventScore) > 0 ? 'color:#67c23a' : Number(dim.eventScore) < 0 ? 'color:#f56c6c' : ''">
176
+                  {{ Number(dim.eventScore) > 0 ? '+' + dim.eventScore : dim.eventScore }}
177
+                </td>
178
+                <td><strong>{{ dim.score }}</strong></td>
179
+                <td>{{ dim.weight }}%</td>
180
+                <td>{{ (Number(dim.score) * Number(dim.weight) / 100).toFixed(1) }}</td>
181
+              </tr>
182
+            </tbody>
183
+          </table>
149 184
         </div>
150 185
 
151 186
         <div class="card training-card">
152
-          <h3 class="card-title">培训情况</h3>
153
-          <div class="training-item">
154
-            <span class="item-label">季度考核:</span>
155
-            <span class="item-value">90分</span>
156
-            <span class="item-value">图像成绩 95分</span>
187
+          <h3 class="card-title">最新考试成绩</h3>
188
+          <div v-if="portrait.examDate || portrait.theoryScore != null">
189
+            <div class="training-item" v-if="portrait.examDate">
190
+              <span class="item-label">考试日期:</span>
191
+              <span class="item-value">{{ portrait.examDate }}</span>
192
+              <span class="item-label" style="margin-left:16px">类别:</span>
193
+              <span class="item-value">{{ portrait.examCategory || '-' }}</span>
194
+              <span class="item-label" style="margin-left:16px">期数:</span>
195
+              <span class="item-value">{{ portrait.examPeriod || '-' }}</span>
196
+            </div>
197
+            <div class="training-item" style="margin-top:12px">
198
+              <span class="item-label">理论成绩:</span>
199
+              <span class="item-value score-badge" :style="examScoreColor(portrait.theoryScore)">
200
+                {{ portrait.theoryScore != null ? portrait.theoryScore + '分' : '-' }}
201
+              </span>
202
+              <span class="item-label" style="margin-left:24px">图像成绩:</span>
203
+              <span class="item-value score-badge" :style="examScoreColor(portrait.imageScore)">
204
+                {{ portrait.imageScore != null ? portrait.imageScore + '分' : '-' }}
205
+              </span>
206
+            </div>
157 207
           </div>
208
+          <div v-else class="empty-text">暂无考试记录</div>
158 209
         </div>
159 210
       </div>
160 211
     </div>
@@ -162,105 +213,147 @@
162 213
 </template>
163 214
 
164 215
 <script setup>
165
-import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
166
-import * as echarts from 'echarts';
167
-
168
-defineOptions({ name: 'EmployeeProfile' });
216
+import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
217
+import * as echarts from 'echarts'
218
+import { Search } from '@element-plus/icons-vue'
219
+import { searchPortraitUsers, getEmployeePortrait } from '@/api/score/index'
220
+
221
+defineOptions({ name: 'EmployeeProfile' })
222
+
223
+const currentTime = ref('year')
224
+const dateRange = ref([])
225
+const personName = ref('')
226
+const portrait = ref(null)
227
+const loading = ref(false)
228
+const abilityChart = ref(null)
229
+let chart = null
230
+
231
+const formatDate = (d) => {
232
+  const dt = new Date(d)
233
+  const y = dt.getFullYear()
234
+  const m = String(dt.getMonth() + 1).padStart(2, '0')
235
+  const dd = String(dt.getDate()).padStart(2, '0')
236
+  return `${y}-${m}-${dd}`
237
+}
169 238
 
170
-const currentTime = ref('year');
171
-const dateRange = ref([]);
172
-const selectedOrg = ref('dept1');
173
-const abilityChart = ref(null);
174
-let chart = null;
239
+const getTimeRange = () => {
240
+  if (currentTime.value === 'custom') {
241
+    if (dateRange.value && dateRange.value.length === 2) {
242
+      return { beginTime: formatDate(dateRange.value[0]), endTime: formatDate(dateRange.value[1]) }
243
+    }
244
+    return {}
245
+  }
246
+  const end = new Date()
247
+  const begin = new Date()
248
+  if (currentTime.value === 'week') begin.setDate(end.getDate() - 7)
249
+  else if (currentTime.value === 'month') begin.setMonth(end.getMonth() - 1)
250
+  else if (currentTime.value === 'quarter') begin.setMonth(end.getMonth() - 3)
251
+  else begin.setFullYear(end.getFullYear() - 1)
252
+  return { beginTime: formatDate(begin), endTime: formatDate(end) }
253
+}
175 254
 
176 255
 const selectTime = (time) => {
177
-  currentTime.value = time;
178
-};
256
+  currentTime.value = time
257
+  if (time !== 'custom') loadPortrait()
258
+}
259
+
260
+const queryUsers = async (query, cb) => {
261
+  if (!query || !query.trim()) { cb([]); return }
262
+  try {
263
+    const res = await searchPortraitUsers(query.trim())
264
+    cb((res.data || []).map(u => ({ ...u, value: u.nickName })))
265
+  } catch (_) {
266
+    cb([])
267
+  }
268
+}
269
+
270
+const handlePersonSelect = (item) => {
271
+  personName.value = item.nickName
272
+  loadPortrait()
273
+}
274
+
275
+const loadPortrait = async () => {
276
+  if (!personName.value) return
277
+  loading.value = true
278
+  try {
279
+    const params = { personName: personName.value, ...getTimeRange() }
280
+    const res = await getEmployeePortrait(params)
281
+    portrait.value = res.data || null
282
+  } catch (_) {
283
+    portrait.value = null
284
+  } finally {
285
+    loading.value = false
286
+  }
287
+}
288
+
289
+const scoreColor = computed(() => {
290
+  const s = Number(portrait.value?.totalScore)
291
+  if (!s) return 'color:#ff9900'
292
+  if (s < 75) return 'color:#f56c6c'
293
+  if (s > 85) return 'color:#67c23a'
294
+  return 'color:#ff9900'
295
+})
296
+
297
+const scoreLevel = computed(() => {
298
+  const s = Number(portrait.value?.totalScore)
299
+  if (!s) return ''
300
+  if (s < 75) return '较差'
301
+  if (s > 85) return '优秀'
302
+  return '良好'
303
+})
304
+
305
+const examScoreColor = (score) => {
306
+  const s = Number(score)
307
+  if (!s) return ''
308
+  if (s >= 98) return 'color:#67c23a;font-weight:bold'
309
+  if (s < 60) return 'color:#f56c6c'
310
+  return 'color:#409eff'
311
+}
179 312
 
180 313
 const initChart = () => {
181
-  if (!abilityChart.value) return;
182
-  chart = echarts.init(abilityChart.value);
314
+  if (!abilityChart.value || !portrait.value) return
315
+  if (chart) { chart.dispose(); chart = null }
316
+  chart = echarts.init(abilityChart.value)
317
+  const dims = portrait.value.dimensions || []
183 318
   const option = {
184 319
     radar: {
185
-      indicator: [
186
-        { name: '安全防控能力', max: 100 },
187
-        { name: '服务响应能力', max: 100 },
188
-        { name: '业务实操能力', max: 100 },
189
-        { name: '作风践行能力', max: 100 },
190
-        { name: '身心调节能力', max: 100 }
191
-      ],
320
+      indicator: dims.map(d => ({ name: d.name, max: 100 })),
192 321
       center: ['50%', '55%'],
193 322
       radius: '70%',
194 323
       splitNumber: 4,
195
-      axisLine: {
196
-        lineStyle: {
197
-          color: '#ddd'
198
-        }
199
-      },
200
-      splitArea: {
201
-        areaStyle: {
202
-          color: ['rgba(255,255,255,0)', 'rgba(255,255,255,0)']
203
-        }
204
-      },
205
-      splitLine: {
206
-        lineStyle: {
207
-          color: '#ddd'
208
-        }
209
-      },
210
-      name: {
211
-        textStyle: {
212
-          color: '#333',
213
-          fontSize: 12
214
-        }
215
-      }
324
+      axisLine: { lineStyle: { color: '#ddd' } },
325
+      splitArea: { areaStyle: { color: ['rgba(255,255,255,0)', 'rgba(255,255,255,0)'] } },
326
+      splitLine: { lineStyle: { color: '#ddd' } },
327
+      name: { textStyle: { color: '#333', fontSize: 12 } }
216 328
     },
217
-    series: [
218
-      {
219
-        type: 'radar',
220
-        data: [
221
-          {
222
-            value: [84, 75, 68, 74, 80],
223
-            name: '个人能力',
224
-            areaStyle: {
225
-              color: 'rgba(80, 180, 255, 0.4)'
226
-            },
227
-            lineStyle: {
228
-              color: '#50b4ff',
229
-              width: 2
230
-            },
231
-            itemStyle: {
232
-              color: '#50b4ff'
233
-            }
234
-          }
235
-        ],
236
-        symbol: 'circle',
237
-        symbolSize: 6
238
-      }
239
-    ]
240
-  };
241
-  chart.setOption(option);
242
-};
243
-
244
-const handleResize = () => {
245
-  if (chart) {
246
-    chart.resize();
329
+    series: [{
330
+      type: 'radar',
331
+      data: [{
332
+        value: dims.map(d => Number(d.score)),
333
+        name: '个人能力',
334
+        areaStyle: { color: 'rgba(80, 180, 255, 0.4)' },
335
+        lineStyle: { color: '#50b4ff', width: 2 },
336
+        itemStyle: { color: '#50b4ff' }
337
+      }],
338
+      symbol: 'circle',
339
+      symbolSize: 6
340
+    }]
247 341
   }
248
-};
342
+  chart.setOption(option)
343
+}
344
+
345
+watch(portrait, (val) => {
346
+  if (val) nextTick(() => initChart())
347
+})
348
+
349
+const handleResize = () => { if (chart) chart.resize() }
249 350
 
250
-onMounted(() => {
251
-  nextTick(() => {
252
-    initChart();
253
-    window.addEventListener('resize', handleResize);
254
-  });
255
-});
351
+onMounted(() => { window.addEventListener('resize', handleResize) })
256 352
 
257 353
 onBeforeUnmount(() => {
258
-  window.removeEventListener('resize', handleResize);
259
-  if (chart) {
260
-    chart.dispose();
261
-    chart = null;
262
-  }
263
-});
354
+  window.removeEventListener('resize', handleResize)
355
+  if (chart) { chart.dispose(); chart = null }
356
+})
264 357
 </script>
265 358
 
266 359
 <style scoped lang="scss">
@@ -284,6 +377,7 @@ onBeforeUnmount(() => {
284 377
     display: flex;
285 378
     align-items: center;
286 379
     gap: 10px;
380
+    flex-wrap: wrap;
287 381
 
288 382
     .label {
289 383
       font-size: 14px;
@@ -293,6 +387,20 @@ onBeforeUnmount(() => {
293 387
   }
294 388
 }
295 389
 
390
+.empty-tip {
391
+  background: #fff;
392
+  border-radius: 8px;
393
+  padding: 60px 20px;
394
+  text-align: center;
395
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
396
+}
397
+
398
+.empty-text {
399
+  color: #999;
400
+  font-size: 13px;
401
+  padding: 10px 0;
402
+}
403
+
296 404
 .main-content {
297 405
   display: flex;
298 406
   flex-direction: column;
@@ -340,14 +448,28 @@ onBeforeUnmount(() => {
340 448
 
341 449
   .avatar-section {
342 450
     flex-shrink: 0;
451
+  }
343 452
 
344
-    .avatar {
345
-      width: 120px;
346
-      height: 120px;
347
-      border-radius: 50%;
348
-      border: 3px solid #409eff;
349
-      object-fit: cover;
350
-    }
453
+  .avatar-img-wrap .avatar {
454
+    width: 120px;
455
+    height: 120px;
456
+    border-radius: 50%;
457
+    border: 3px solid #409eff;
458
+    object-fit: cover;
459
+  }
460
+
461
+  .avatar-placeholder {
462
+    width: 120px;
463
+    height: 120px;
464
+    border-radius: 50%;
465
+    border: 3px solid #409eff;
466
+    background: linear-gradient(135deg, #409eff, #66b1ff);
467
+    display: flex;
468
+    align-items: center;
469
+    justify-content: center;
470
+    font-size: 36px;
471
+    font-weight: bold;
472
+    color: #fff;
351 473
   }
352 474
 
353 475
   .info-section {
@@ -364,30 +486,13 @@ onBeforeUnmount(() => {
364 486
       display: grid;
365 487
       grid-template-columns: repeat(2, 1fr);
366 488
       gap: 10px;
367
-      margin-bottom: 15px;
368 489
 
369 490
       .info-item {
370 491
         font-size: 14px;
371 492
         color: #666;
372 493
 
373
-        .label {
374
-          color: #999;
375
-        }
376
-        .value {
377
-          color: #333;
378
-          font-weight: 500;
379
-        }
380
-      }
381
-    }
382
-
383
-    .tags {
384
-      display: flex;
385
-      align-items: center;
386
-      gap: 8px;
387
-
388
-      .label {
389
-        font-size: 14px;
390
-        color: #999;
494
+        .label { color: #999; }
495
+        .value { color: #333; font-weight: 500; }
391 496
       }
392 497
     }
393 498
   }
@@ -403,17 +508,19 @@ onBeforeUnmount(() => {
403 508
   .score-display {
404 509
     flex: 1;
405 510
     display: flex;
511
+    flex-direction: column;
406 512
     align-items: center;
407 513
     justify-content: center;
408 514
 
409 515
     .score {
410
-      font-size: 48px;
516
+      font-size: 52px;
411 517
       font-weight: bold;
412
-      color: #ff9900;
413 518
     }
414
-    .plus {
415
-      font-size: 24px;
416
-      color: #67c23a;
519
+
520
+    .score-level {
521
+      font-size: 16px;
522
+      margin-top: 8px;
523
+      font-weight: 500;
417 524
     }
418 525
   }
419 526
 }
@@ -428,17 +535,10 @@ onBeforeUnmount(() => {
428 535
       color: #666;
429 536
       border-bottom: 1px dashed #eee;
430 537
 
431
-      &:last-child {
432
-        border-bottom: none;
433
-      }
538
+      &:last-child { border-bottom: none; }
434 539
 
435
-      .label {
436
-        color: #999;
437
-      }
438
-      .value {
439
-        color: #333;
440
-        font-weight: 500;
441
-      }
540
+      .label { color: #999; }
541
+      .value { color: #333; font-weight: 500; }
442 542
     }
443 543
   }
444 544
 }
@@ -469,45 +569,33 @@ onBeforeUnmount(() => {
469 569
 
470 570
 .award-card {
471 571
   flex: 1;
572
+}
472 573
 
473
-  .award-table {
474
-    width: 100%;
475
-    border-collapse: collapse;
476
-    font-size: 13px;
574
+.award-table {
575
+  width: 100%;
576
+  border-collapse: collapse;
577
+  font-size: 13px;
477 578
 
478
-    th, td {
479
-      padding: 10px 8px;
480
-      text-align: center;
481
-      border: 1px solid #ddd;
482
-    }
579
+  th, td {
580
+    padding: 10px 8px;
581
+    text-align: center;
582
+    border: 1px solid #ddd;
583
+  }
483 584
 
484
-    th {
485
-      background: #ffeae8;
486
-      color: #333;
487
-      font-weight: 500;
488
-    }
585
+  th {
586
+    background: #ffeae8;
587
+    color: #333;
588
+    font-weight: 500;
589
+  }
489 590
 
490
-    td {
491
-      background: #fff;
492
-      color: #666;
493
-    }
591
+  td {
592
+    background: #fff;
593
+    color: #666;
494 594
   }
495 595
 }
496 596
 
497
-.position-card {
498
-  flex: 1;
499
-
500
-  .position-list {
501
-    .position-item {
502
-      padding: 12px 15px;
503
-      background: #fceee7;
504
-      border-radius: 8px;
505
-      margin-bottom: 8px;
506
-      font-size: 14px;
507
-      color: #333;
508
-      text-align: center;
509
-    }
510
-  }
597
+.dim-detail-card {
598
+  flex: 1.5;
511 599
 }
512 600
 
513 601
 .training-card {
@@ -519,13 +607,11 @@ onBeforeUnmount(() => {
519 607
     border-radius: 8px;
520 608
     font-size: 14px;
521 609
 
522
-    .item-label {
523
-      color: #666;
524
-    }
525
-    .item-value {
526
-      color: #333;
527
-      font-weight: 500;
528
-      margin-left: 8px;
610
+    .item-label { color: #666; }
611
+    .item-value { color: #333; font-weight: 500; margin-left: 4px; }
612
+
613
+    .score-badge {
614
+      font-size: 16px;
529 615
     }
530 616
   }
531 617
 }