ScoringEditView.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <template>
  2. <div class="page">
  3. <div class="page-hero">
  4. <div class="hero-info">
  5. <h2>绩效考评</h2>
  6. <p>{{ data.targetUser?.realName }} · {{ data.period?.name }}</p>
  7. </div>
  8. <el-tag size="large" effect="dark" round>{{ data.period?.startDate }} ~ {{ data.period?.endDate }}</el-tag>
  9. </div>
  10. <div class="same-screen" v-if="data.targetUser">
  11. <!-- Left: OKR Records + Self Assessment -->
  12. <div class="left-panel">
  13. <!-- OKR execution records -->
  14. <div class="panel-head">当期 OKR 完整记录</div>
  15. <div v-if="!okrRecords.length" class="panel-empty">暂无 OKR 记录</div>
  16. <div v-for="o in okrRecords" :key="o.id" class="okr-record-card">
  17. <!-- O header -->
  18. <div class="orr-o-head">
  19. <span class="orr-o-badge">O</span>
  20. <span class="orr-o-title">{{ o.title }}</span>
  21. <el-tag v-if="o.reviewedAt" size="small" type="success" effect="plain" round>已通过</el-tag>
  22. </div>
  23. <div class="orr-o-meta" v-if="o.reviewedAt">
  24. {{ o.reviewerName }} 审核 · {{ o.reviewedAt?.slice(0, 10) }}
  25. </div>
  26. <!-- KR list -->
  27. <div class="orr-krs">
  28. <div v-for="(kr, i) in o.keyResults" :key="kr.id" class="orr-kr-card">
  29. <!-- KR header -->
  30. <div class="orr-kr-header">
  31. <span class="orr-kr-num">KR{{ i + 1 }}</span>
  32. <span class="orr-kr-title">{{ kr.title }}</span>
  33. <el-tag :type="krStatusTag(kr.status)" size="small" effect="plain" round>
  34. {{ krStatusLabel(kr.status) }}
  35. </el-tag>
  36. </div>
  37. <!-- KR metrics -->
  38. <div class="orr-kr-metrics">
  39. <div class="orkm-item">
  40. <span class="orkm-label">进度</span>
  41. <span class="orkm-value">{{ kr.currentValue }} / {{ kr.targetValue }}</span>
  42. </div>
  43. <div class="orkm-item">
  44. <span class="orkm-label">完成率</span>
  45. <span class="orkm-value" :style="{ color: krColor(kr.status) }">{{ krPercent(kr).toFixed(0) }}%</span>
  46. </div>
  47. <div class="orkm-item" v-if="kr.weight">
  48. <span class="orkm-label">权重</span>
  49. <span class="orkm-value">{{ kr.weight }}</span>
  50. </div>
  51. </div>
  52. <!-- Progress bar -->
  53. <el-progress :percentage="krPercent(kr)" :color="krColor(kr.status)" :stroke-width="8" />
  54. <!-- Progress logs -->
  55. <div v-if="kr.progressLogs?.length" class="orr-kr-logs">
  56. <div class="orkl-head">更新记录</div>
  57. <div v-for="log in kr.progressLogs.slice(0, 5)" :key="log.id" class="orkl-line">
  58. <span class="orkl-time">{{ fmtTime(log.createdAt) }}</span>
  59. <span class="orkl-change">{{ log.oldValue || 0 }} → {{ log.newValue }}</span>
  60. <span class="orkl-status">{{ krStatusLabel(log.newStatus) }}</span>
  61. <span class="orkl-remark" v-if="log.remark">{{ log.remark }}</span>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. </div>
  67. <!-- Employee Self Assessment (mirrors right-side scoring structure) -->
  68. <div class="panel-divider"></div>
  69. <div class="panel-head">员工自评</div>
  70. <div v-if="!data.selfScore" class="panel-empty">员工尚未提交自评</div>
  71. <template v-else>
  72. <!-- 1. OKR self scores -->
  73. <div class="self-block">
  74. <div class="self-block-title">OKR 目标达成自评 <span class="sb-sub">(满分 {{ computedOkrMaxScore }})</span></div>
  75. <div v-if="selfKrScores.length" class="self-kr-list">
  76. <div v-for="(sk, i) in selfKrScores" :key="i" class="self-kr-item">
  77. <span class="ski-num">KR{{ i+1 }}</span>
  78. <span class="ski-title">{{ sk.title }}</span>
  79. <span class="ski-score">{{ sk.score }}</span>
  80. </div>
  81. </div>
  82. <div class="self-subtotal">OKR 自评 = <strong>{{ selfOkrTotal.toFixed(1) }} 分</strong></div>
  83. </div>
  84. <!-- 2. Work result self scores -->
  85. <div class="self-block">
  86. <div class="self-block-title">工作成果自评 <span class="sb-sub">(满分 40)</span></div>
  87. <div v-if="selfDimensions.length" class="self-dim-list">
  88. <div v-for="d in selfDimensions" :key="d.name" class="self-dim-item">
  89. <div class="sdi-head">
  90. <span class="sdi-name">{{ d.name }}</span>
  91. <span class="sdi-score">{{ d.score }} / {{ d.maxScore }}</span>
  92. </div>
  93. <div class="sdi-desc" v-if="d.description">{{ d.description }}</div>
  94. </div>
  95. </div>
  96. <div class="self-subtotal">工作成果自评 = <strong>{{ selfDimTotal.toFixed(1) }} 分</strong></div>
  97. </div>
  98. <!-- 3. Bonus/Penalty -->
  99. <div class="self-block">
  100. <div class="self-block-title">⚡ 加减分</div>
  101. <div class="self-bp">
  102. <span>加分 <strong>+{{ data.selfScore.bonus }}</strong></span>
  103. <span>扣分 <strong>-{{ data.selfScore.penalty }}</strong></span>
  104. <span v-if="selfRemark">说明:{{ selfRemark }}</span>
  105. </div>
  106. </div>
  107. <!-- Total -->
  108. <div class="self-total-bar">
  109. 员工自评总分 <strong>{{ data.selfScore.total }} / 100</strong>
  110. <span class="stb-grade">(自评 {{ selfGrade }})</span>
  111. </div>
  112. </template>
  113. </div>
  114. <!-- Right: Superior Scoring -->
  115. <div class="score-panel">
  116. <div class="panel-head">考评打分</div>
  117. <!-- KR Scores -->
  118. <div class="score-card">
  119. <div class="sc-header">OKR 目标达成 <span class="sc-sub">逐条 KR 打分,满分 60</span></div>
  120. <div class="kr-score-list">
  121. <div v-for="(ks, i) in krScores" :key="i" class="kr-score-row">
  122. <div class="ksr-label">
  123. <span class="ksr-num">KR{{ i+1 }}</span>
  124. <span class="ksr-title">{{ ks.title || '(未配置)' }}</span>
  125. <span v-if="selfKrScore(i) !== null" class="ksr-self-ref">自评 {{ selfKrScore(i) }}</span>
  126. </div>
  127. <div class="ksr-inputs">
  128. <el-select v-model="ks.score" size="default" style="width:120px">
  129. <el-option label="1.0 · 卓越" :value="1.0" />
  130. <el-option label="0.8 · 良好" :value="0.8" />
  131. <el-option label="0.6 · 达标" :value="0.6" />
  132. <el-option label="0.3 · 少量推进" :value="0.3" />
  133. <el-option label="0.0 · 未做" :value="0.0" />
  134. </el-select>
  135. <span class="ksr-eq">= {{ (ks.score * 60 / (okrRecords[0]?.keyResults?.length || 3)).toFixed(1) }} 分</span>
  136. </div>
  137. </div>
  138. </div>
  139. <div class="okr-subtotal">
  140. OKR 得分 = 平均 {{ krAvg.toFixed(2) }} × 60 = <strong>{{ computedOkrScore.toFixed(1) }} / 60</strong>
  141. </div>
  142. </div>
  143. <!-- Work Result (Dimensions) -->
  144. <div class="score-card">
  145. <div class="sc-header">工作成果评价 <span class="sc-sub">满分 40</span></div>
  146. <div v-for="(dim, i) in dimScores" :key="i" class="dim-row">
  147. <div class="dim-head">
  148. <span class="dim-num">{{ i+1 }}</span>
  149. <span class="dim-name">{{ dim.name }}</span>
  150. <span class="dim-max">满分 {{ dim.maxScore }} 分</span>
  151. <span v-if="selfDimScore(dim.name) !== null" class="dim-self-ref">自评 {{ selfDimScore(dim.name) }}</span>
  152. </div>
  153. <div v-if="selfDimDesc(dim.name)" class="dim-self-desc">{{ selfDimDesc(dim.name) }}</div>
  154. <el-input-number v-model="dim.score" :min="0" :max="dim.maxScore" :precision="1" size="small" controls-position="right" style="width:130px" />
  155. <span class="dim-score-unit">/ {{ dim.maxScore }} 分</span>
  156. </div>
  157. <div class="okr-subtotal">
  158. 工作成果得分 = <strong>{{ dimTotal.toFixed(1) }} / 40</strong>
  159. </div>
  160. </div>
  161. <!-- Bonus/Penalty -->
  162. <div class="score-card">
  163. <div class="sc-header">⚡ 专项加减分</div>
  164. <div class="sc-body">
  165. <div class="sc-inline">
  166. <span class="sc-label">加分(上限5)</span>
  167. <el-input-number v-model="form.bonus" :min="0" :max="5" :precision="1" size="large" controls-position="right" style="width:120px" />
  168. </div>
  169. <div class="sc-inline">
  170. <span class="sc-label">扣分(无下限)</span>
  171. <el-input-number v-model="form.penalty" :min="0" :precision="1" size="large" controls-position="right" style="width:120px" />
  172. </div>
  173. <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="加减分说明(必填)" style="margin-top:8px" />
  174. </div>
  175. </div>
  176. <!-- Result -->
  177. <div class="result-preview">
  178. <div class="rp-formula">OKR×0.6 + 工作成果×0.4 + 加分 − 扣分</div>
  179. <div class="rp-score">{{ computedTotal }} <small>/ 100</small></div>
  180. <div class="rp-grade">评级 <strong>{{ computedGrade }}</strong></div>
  181. </div>
  182. <el-button type="primary" size="large" class="submit-btn" @click="handleSubmit" :loading="submitting" round>
  183. 提交绩效考评
  184. </el-button>
  185. </div>
  186. </div>
  187. </div>
  188. </template>
  189. <script setup>
  190. import { ref, reactive, computed, onMounted } from 'vue'
  191. import { useRoute, useRouter } from 'vue-router'
  192. import { scoreApi, dimensionApi } from '../../api'
  193. import { ElMessage } from 'element-plus'
  194. const route = useRoute()
  195. const router = useRouter()
  196. const data = ref({})
  197. const submitting = ref(false)
  198. const form = reactive({ bonus: 0, penalty: 0, remark: '' })
  199. const krScores = ref([])
  200. const dimensions = ref([])
  201. const dimScores = ref([])
  202. const okrRecords = computed(() => data.value.okrRecords || [])
  203. const krAvg = computed(() => {
  204. if (!krScores.value.length) return 0
  205. const sum = krScores.value.reduce((a, b) => a + b.score, 0)
  206. return sum / krScores.value.length
  207. })
  208. const computedOkrScore = computed(() => {
  209. const okrs = okrRecords.value
  210. if (okrs.length && okrs[0].keyResults?.length) {
  211. const n = okrs[0].keyResults.length
  212. const sum = krScores.value.reduce((a, b) => a + b.score, 0)
  213. return Math.round(sum / n * 60 * 10) / 10
  214. }
  215. return 0
  216. })
  217. const dimTotal = computed(() => Math.round(dimScores.value.reduce((s, d) => s + (d.score || 0), 0) * 10) / 10)
  218. const computedTotal = computed(() => {
  219. return Math.round((computedOkrScore.value + dimTotal.value + form.bonus - form.penalty) * 10) / 10
  220. })
  221. const computedGrade = computed(() => {
  222. const t = computedTotal.value
  223. if (t >= 90) return 'S'
  224. if (t >= 75) return 'A'
  225. if (t >= 60) return 'B'
  226. return 'C'
  227. })
  228. // Parse self-assessment from commentsJson
  229. const selfData = computed(() => {
  230. if (!data.value.selfScore?.commentsJson) return null
  231. try { return JSON.parse(data.value.selfScore.commentsJson) } catch { return null }
  232. })
  233. const selfKrScores = computed(() => selfData.value?.krScores || [])
  234. const selfDimensions = computed(() => selfData.value?.dimensions || [])
  235. const selfRemark = computed(() => selfData.value?.remark || '')
  236. const selfOkrTotal = computed(() => {
  237. if (!selfKrScores.value.length) return 0
  238. const n = okrRecords.value[0]?.keyResults?.length || selfKrScores.value.length
  239. const sum = selfKrScores.value.reduce((a, b) => a + (b.score || 0), 0)
  240. return Math.round(sum / n * 60 * 10) / 10
  241. })
  242. const selfDimTotal = computed(() => {
  243. return Math.round(selfDimensions.value.reduce((s, d) => s + (d.score || 0), 0) * 10) / 10
  244. })
  245. const selfGrade = computed(() => {
  246. const t = (data.value.selfScore?.total) || 0
  247. if (t >= 90) return 'S'
  248. if (t >= 75) return 'A'
  249. if (t >= 60) return 'B'
  250. return 'C'
  251. })
  252. const computedOkrMaxScore = computed(() => {
  253. const okrs = okrRecords.value
  254. if (okrs.length && okrs[0]?.keyResults?.length) return 60
  255. return 60
  256. })
  257. function selfKrScore(idx) {
  258. return selfKrScores.value[idx]?.score ?? null
  259. }
  260. function selfDimScore(name) {
  261. const found = selfDimensions.value.find(d => d.name === name)
  262. return found ? found.score : null
  263. }
  264. function selfDimDesc(name) {
  265. const found = selfDimensions.value.find(d => d.name === name)
  266. return found?.description || ''
  267. }
  268. onMounted(async () => {
  269. data.value = await scoreApi.getPage(route.query.periodId, route.params.userId)
  270. const okrs = okrRecords.value
  271. if (okrs.length && okrs[0].keyResults?.length) {
  272. krScores.value = okrs[0].keyResults.map(kr => ({ title: kr.title, score: 0.6 }))
  273. }
  274. // Load dimensions for the scoring form
  275. dimensions.value = await dimensionApi.list()
  276. dimScores.value = dimensions.value.map(d => ({
  277. id: d.id, name: d.name, maxScore: d.maxScore,
  278. score: selfDimScore(d.name) ?? Math.round(d.maxScore * 0.7 * 10) / 10
  279. }))
  280. })
  281. async function handleSubmit() {
  282. if (!form.remark) { ElMessage.warning('请填写加减分备注'); return }
  283. submitting.value = true
  284. try {
  285. const dimItems = dimScores.value.map(d => ({
  286. dimensionId: d.id, name: d.name, maxScore: d.maxScore,
  287. score: d.score
  288. }))
  289. await scoreApi.submitSuperior({
  290. periodId: route.query.periodId,
  291. userId: Number(route.params.userId),
  292. okrScore: computedOkrScore.value,
  293. kpiScore: dimTotal.value,
  294. bonus: form.bonus,
  295. penalty: form.penalty,
  296. commentsJson: JSON.stringify({
  297. krScores: krScores.value,
  298. dimensions: dimItems,
  299. remark: form.remark
  300. })
  301. })
  302. ElMessage.success('考评已提交')
  303. router.push('/scoring')
  304. } finally { submitting.value = false }
  305. }
  306. function krPercent(kr) { if (!kr.targetValue || kr.targetValue === 0) return 0; return Math.min((kr.currentValue / kr.targetValue) * 100, 100) }
  307. function krColor(s) { return { COMPLETED: '#27ae60', BLOCKED: '#e67e22', IN_PROGRESS: '#3ad4d8', NOT_STARTED: '#c0c4cc' }[s] || '#c0c4cc' }
  308. function krStatusLabel(s) { return { NOT_STARTED: '未开始', IN_PROGRESS: '进行中', COMPLETED: '已完成', BLOCKED: '阻塞' }[s] || s }
  309. function krStatusTag(s) { return { COMPLETED: 'success', BLOCKED: 'danger', IN_PROGRESS: '', NOT_STARTED: 'info' }[s] || 'info' }
  310. function fmtTime(t) { if (!t) return ''; return t.slice(0, 16).replace('T', ' ') }
  311. </script>
  312. <style scoped>
  313. .page { max-width: 1300px; }
  314. .page-hero { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; }
  315. .hero-info h2 { font-size: 28px; font-weight: 700; color: #1a1a2e; margin: 0; }
  316. .hero-info p { font-size: 14px; color: #888; margin: 4px 0 0; }
  317. .same-screen { display: flex; gap: 20px; align-items: flex-start; }
  318. .left-panel {
  319. flex: 1; background: #fff; border-radius: 12px; padding: 20px 24px;
  320. box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0;
  321. max-height: calc(100vh - 160px); overflow-y: auto;
  322. }
  323. .score-panel { width: 480px; flex-shrink: 0; max-height: calc(100vh - 160px); overflow-y: auto; }
  324. .panel-head { font-size: 15px; font-weight: 700; color: #1a1a2e; margin-bottom: 16px; }
  325. .panel-empty { color: #ccc; text-align: center; padding: 20px; font-size: 13px; }
  326. /* OKR Record Cards */
  327. .okr-record-card { background: #fff; border-radius: 10px; margin-bottom: 14px; border: 1px solid #e8eaed; overflow: hidden; }
  328. .orr-o-head { display: flex; align-items: center; gap: 10px; padding: 14px 16px 10px; background: #fafbfc; }
  329. .orr-o-badge { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 6px; background: #2b1f99; color: #fff; font-size: 13px; font-weight: 700; flex-shrink: 0; }
  330. .orr-o-title { font-size: 14px; font-weight: 700; color: #1a1a2e; flex: 1; }
  331. .orr-o-meta { font-size: 11px; color: #27ae60; padding: 0 16px 6px; background: #fafbfc; }
  332. .orr-krs { padding: 8px 12px 12px; display: flex; flex-direction: column; gap: 8px; }
  333. .orr-kr-card { padding: 12px 14px; background: #f8f9fb; border-radius: 8px; border: 1px solid #eef0f2; }
  334. .orr-kr-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
  335. .orr-kr-num { font-size: 12px; font-weight: 700; color: #3ad4d8; width: 30px; flex-shrink: 0; }
  336. .orr-kr-title { flex: 1; font-size: 13px; color: #333; font-weight: 500; }
  337. .orr-kr-metrics { display: flex; gap: 20px; margin-bottom: 8px; }
  338. .orkm-item { display: flex; flex-direction: column; }
  339. .orkm-label { font-size: 10px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; }
  340. .orkm-value { font-size: 14px; font-weight: 600; color: #333; }
  341. .orr-kr-logs { margin-top: 10px; padding-top: 8px; border-top: 1px dashed #ddd; }
  342. .orkl-head { font-size: 11px; font-weight: 600; color: #888; margin-bottom: 4px; }
  343. .orkl-line { display: flex; align-items: center; gap: 8px; font-size: 11px; padding: 2px 0; flex-wrap: nowrap; }
  344. .orkl-time { color: #aaa; font-family: monospace; width: 120px; flex-shrink: 0; }
  345. .orkl-change { color: #555; font-family: monospace; width: 55px; flex-shrink: 0; }
  346. .orkl-status { color: #888; width: 42px; flex-shrink: 0; text-align: center; }
  347. .orkl-remark { color: #999; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
  348. .panel-divider { border-top: 2px solid #e8eaed; margin: 20px 0 16px; }
  349. /* Self-assessment blocks */
  350. .self-block { padding: 10px 0; border-top: 1px solid #f0f0f0; }
  351. .self-block:first-of-type { border-top: none; padding-top: 0; }
  352. .self-block-title { font-size: 13px; font-weight: 600; color: #1a1a2e; margin-bottom: 8px; }
  353. .sb-sub { font-size: 11px; color: #aaa; font-weight: 400; }
  354. .self-kr-list { display: flex; flex-direction: column; gap: 4px; }
  355. .self-kr-item { display: flex; align-items: center; gap: 8px; padding: 4px 8px; background: #f5f3ff; border-radius: 6px; font-size: 12px; }
  356. .ski-num { font-weight: 700; color: #3ad4d8; width: 28px; flex-shrink: 0; }
  357. .ski-title { flex: 1; color: #555; }
  358. .ski-score { font-weight: 600; color: #2b1f99; flex-shrink: 0; }
  359. .self-subtotal { text-align: right; font-size: 12px; color: #2b1f99; margin-top: 6px; padding-top: 4px; border-top: 1px dashed #e8eaed; }
  360. .self-dim-list { display: flex; flex-direction: column; gap: 6px; }
  361. .self-dim-item { padding: 6px 10px; background: #f5f3ff; border-radius: 6px; }
  362. .sdi-head { display: flex; justify-content: space-between; align-items: center; }
  363. .sdi-name { font-size: 12px; font-weight: 600; color: #333; }
  364. .sdi-score { font-size: 13px; font-weight: 600; color: #2b1f99; }
  365. .sdi-desc { font-size: 11px; color: #888; margin-top: 4px; line-height: 1.5; }
  366. .self-bp { display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; color: #666; }
  367. .self-bp strong { color: #1a1a2e; }
  368. .self-total-bar {
  369. margin-top: 12px; padding: 12px 16px;
  370. background: #f4f2f9; border-radius: 10px;
  371. text-align: center; font-size: 14px; color: #555;
  372. border: 1px solid #dcd4f0;
  373. }
  374. .self-total-bar strong { font-size: 20px; color: #2b1f99; }
  375. .stb-grade { font-size: 13px; color: #888; }
  376. .score-card { background: #fff; border-radius: 10px; padding: 16px 20px; margin-bottom: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.04); border: 1px solid #f0f0f0; }
  377. .sc-header { font-weight: 600; color: #1a1a2e; font-size: 14px; margin-bottom: 10px; }
  378. .sc-sub { font-size: 12px; color: #aaa; font-weight: 400; }
  379. .sc-body { }
  380. .sc-label { font-size: 13px; color: #888; margin-right: 12px; }
  381. .sc-inline { display: flex; align-items: center; margin-bottom: 8px; }
  382. .sc-unit { font-size: 14px; color: #999; margin-left: 8px; }
  383. .kr-score-row { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-top: 1px solid #f5f5f5; }
  384. .kr-score-row:first-child { border-top: none; }
  385. .ksr-label { flex: 1; min-width: 0; }
  386. .ksr-num { font-size: 12px; font-weight: 700; color: #3ad4d8; margin-right: 6px; }
  387. .ksr-title { font-size: 12px; color: #555; }
  388. .ksr-self-ref { font-size: 10px; color: #3498db; display: block; }
  389. .ksr-inputs { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
  390. .ksr-eq { font-size: 12px; color: #999; white-space: nowrap; }
  391. .okr-subtotal { text-align: right; font-size: 13px; color: #2b1f99; padding-top: 8px; border-top: 1px dashed #e0e0e0; }
  392. .dim-row { padding: 10px 0; border-top: 1px solid #f5f5f5; }
  393. .dim-row:first-child { border-top: none; }
  394. .dim-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  395. .dim-num { width: 20px; height: 20px; border-radius: 50%; background: #2b1f99; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; }
  396. .dim-name { font-size: 13px; font-weight: 600; color: #333; }
  397. .dim-max { font-size: 11px; color: #999; }
  398. .dim-self-ref { font-size: 11px; color: #3498db; margin-left: auto; }
  399. .dim-self-desc { font-size: 11px; color: #999; margin-bottom: 4px; padding: 4px 6px; background: #f5f3ff; border-radius: 4px; }
  400. .dim-score-unit { font-size: 13px; color: #999; margin-left: 6px; }
  401. .result-preview {
  402. background: #f4f2f9; border-radius: 12px;
  403. padding: 20px; text-align: center; border: 1px solid #dcd4f0; margin-bottom: 16px;
  404. }
  405. .rp-formula { font-size: 12px; color: #888; }
  406. .rp-score { font-size: 40px; font-weight: 700; color: #2b1f99; margin: 6px 0; line-height: 1; }
  407. .rp-score small { font-size: 18px; color: #999; font-weight: 400; }
  408. .rp-grade { font-size: 14px; color: #666; }
  409. .submit-btn { width: 100%; background: #2b1f99; border: none; height: 48px; font-size: 15px; }
  410. @media (max-width: 1100px) {
  411. .same-screen { flex-direction: column; }
  412. .left-panel, .score-panel { width: 100%; max-height: none; }
  413. }
  414. </style>