522 lines
22 KiB
Vue
522 lines
22 KiB
Vue
<template>
|
||
<div class="rank">
|
||
<!-- 顶部工具栏 -->
|
||
<div class="toolbar">
|
||
<div class="left">
|
||
<el-date-picker v-model="query.period" type="month" format="yyyy-MM" value-format="yyyy-MM"
|
||
size="small" @change="load" style="width:160px;" />
|
||
<el-input v-model="query.nameLike" placeholder="搜索姓名" clearable size="small" style="width:200px;"
|
||
@keyup.enter.native="load" @clear="load">
|
||
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
||
</el-input>
|
||
<el-button size="small" type="primary" icon="el-icon-search" @click="load">查询</el-button>
|
||
</div>
|
||
<div class="right">
|
||
<div class="filter-tabs">
|
||
<span class="tab" :class="{active: filter==='all'}" @click="filter='all'">全部 {{ list.length }}</span>
|
||
<span class="tab ok" :class="{active: filter==='A'}" @click="filter='A'">A {{ countByLetter('A') }}</span>
|
||
<span class="tab b" :class="{active: filter==='B'}" @click="filter='B'">B {{ countByLetter('B') }}</span>
|
||
<span class="tab c" :class="{active: filter==='C'}" @click="filter='C'">C {{ countByLetter('C') }}</span>
|
||
<span class="tab d" :class="{active: filter==='D'}" @click="filter='D'">D {{ countByLetter('D') }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 前三名领奖台 -->
|
||
<div v-if="!filter || filter==='all'" class="podium" v-loading="loading">
|
||
<template v-for="(row, idx) in podium">
|
||
<div :key="row.user_id" class="podium-slot" :class="'p-' + (idx+1)">
|
||
<div class="medal">
|
||
<i class="el-icon-trophy"></i>
|
||
<span class="rank-text">No.{{ idx+1 }}</span>
|
||
</div>
|
||
<div class="podium-card" :class="cardClass(row.grade)">
|
||
<div class="p-grade" :class="gradeClass(row.grade)">{{ row.grade }}</div>
|
||
<div class="p-name">{{ row.user_nick }}</div>
|
||
<div class="p-score">
|
||
<span class="num" :class="totalClass(row.total_score)">{{ row.total_score }}</span>
|
||
<span class="div">/ 100</span>
|
||
</div>
|
||
<div class="p-mini">
|
||
<span>基础 {{ row.base_score }}</span>
|
||
<span v-if="row.reward > 0" class="ok">+{{ row.reward }}</span>
|
||
<span v-if="row.deduction > 0" class="bad">-{{ row.deduction }}</span>
|
||
<span>主观 {{ row.bonus }}</span>
|
||
</div>
|
||
<div class="p-actions" v-if="canScore">
|
||
<el-button size="mini" type="primary" plain @click="openScore(row)">打分</el-button>
|
||
<el-button size="mini" type="warning" plain @click="openAdjust(row)">加减分</el-button>
|
||
</div>
|
||
<el-button size="mini" type="text" @click="openDetail(row)">查看流水 →</el-button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 其余排名表格 -->
|
||
<div class="rest" v-loading="loading">
|
||
<el-table :data="restList" border stripe :row-class-name="rowClass">
|
||
<el-table-column label="排名" type="index" width="70" align="center" :index="indexFn" />
|
||
<el-table-column label="员工" prop="user_nick" min-width="140">
|
||
<template slot-scope="{row}">
|
||
<span class="user-cell">
|
||
<span class="dot" :class="gradeDot(row.grade)"></span>
|
||
{{ row.user_nick }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="等级" width="80" align="center">
|
||
<template slot-scope="{row}">
|
||
<span class="grade-tag" :class="gradeClass(row.grade)">{{ row.grade }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="总分" width="100" align="center" sortable prop="total_score">
|
||
<template slot-scope="{row}">
|
||
<b :class="totalClass(row.total_score)">{{ row.total_score }}</b>
|
||
<span class="of">/100</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="基础" prop="base_score" width="80" align="center" />
|
||
<el-table-column label="加" width="80" align="center">
|
||
<template slot-scope="{row}">
|
||
<span v-if="row.reward > 0" class="ok">+{{ row.reward }}</span>
|
||
<span v-else class="muted">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="扣" width="80" align="center">
|
||
<template slot-scope="{row}">
|
||
<span v-if="row.deduction > 0" class="bad">-{{ row.deduction }}</span>
|
||
<span v-else class="muted">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="主观" width="110" align="center">
|
||
<template slot-scope="{row}">
|
||
<span :style="(row.bonus||0) > 0 ? 'color:#67c23a' : (row.bonus||0) < 0 ? 'color:#f56c6c' : ''">
|
||
{{ (row.bonus || 0) > 0 ? '+' : '' }}{{ row.bonus || 0 }}
|
||
</span>
|
||
<el-tag v-if="row.subjective_set !== 1 && row.subjectiveSet !== 1"
|
||
size="mini" type="info" effect="plain" style="margin-left:4px;">待评</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||
<template slot-scope="{row}">
|
||
<el-button type="text" size="mini" @click="openDetail(row)">流水</el-button>
|
||
<el-button type="text" size="mini" style="color:#409EFF" @click="openScore(row)" v-if="canScore">打分</el-button>
|
||
<el-button type="text" size="mini" style="color:#e6a23c" @click="openAdjust(row)" v-if="canScore">加/扣</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty v-if="!loading && !restList.length && !podium.length" description="本周期暂无数据" />
|
||
</div>
|
||
|
||
<!-- 加减分弹窗 -->
|
||
<el-dialog title="手动加减分" :visible.sync="adjustOpen" width="500px" append-to-body :close-on-click-modal="false">
|
||
<div v-if="adjustForm.userId" class="form-body">
|
||
<div class="form-user">
|
||
<div class="u-name">{{ adjustForm.userName }}</div>
|
||
<div class="u-period">{{ query.period }} 月度</div>
|
||
</div>
|
||
<el-form ref="adjustFormRef" :model="adjustForm" :rules="adjustRules" label-width="90px" size="small">
|
||
<el-form-item label="操作">
|
||
<el-radio-group v-model="adjustForm.dir">
|
||
<el-radio-button :label="-1"><i class="el-icon-plus"></i> 加分</el-radio-button>
|
||
<el-radio-button :label="1"><i class="el-icon-minus"></i> 扣分</el-radio-button>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="分值" prop="absPoints">
|
||
<el-input-number v-model="adjustForm.absPoints" :min="1" :max="40" size="small" />
|
||
<span class="preview" :class="adjustForm.dir < 0 ? 'ok' : 'bad'">
|
||
{{ adjustForm.dir < 0 ? '+' : '−' }}{{ adjustForm.absPoints }} 分
|
||
</span>
|
||
</el-form-item>
|
||
<el-form-item label="原因" prop="reason" required>
|
||
<el-input v-model="adjustForm.reason" type="textarea" :rows="3" maxlength="200" show-word-limit
|
||
placeholder="必填:会写入流水留痕,员工可以在「我的绩效」看到" />
|
||
</el-form-item>
|
||
<el-form-item label="操作人">
|
||
<span class="op-name">{{ currentUserName }}(自动)</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<div slot="footer">
|
||
<el-button @click="adjustOpen = false">取消</el-button>
|
||
<el-button type="primary" :loading="adjustSubmitting" @click="submitAdjust">
|
||
{{ adjustForm.dir < 0 ? '保存(加分)' : '保存(扣分)' }}
|
||
</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 高总主观分弹窗 -->
|
||
<el-dialog title="高总主观打分" :visible.sync="scoreOpen" width="520px" append-to-body :close-on-click-modal="false">
|
||
<div v-if="scoreForm.userId" class="form-body">
|
||
<div class="form-user">
|
||
<div class="u-name">{{ scoreForm.userName }}</div>
|
||
<div class="u-period">{{ query.period }} 月度</div>
|
||
</div>
|
||
<div class="info-grid">
|
||
<div class="info-tile">
|
||
<div class="i-lbl">基础</div>
|
||
<div class="i-val">{{ scoreForm.baseScore }}</div>
|
||
</div>
|
||
<div class="info-tile">
|
||
<div class="i-lbl">扣分</div>
|
||
<div class="i-val bad">-{{ scoreForm.deduction }}</div>
|
||
</div>
|
||
<div class="info-tile">
|
||
<div class="i-lbl">加分</div>
|
||
<div class="i-val ok">+{{ scoreForm.reward || 0 }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="slider-label">主观分(-20 ~ +20,默认 0 = 未评价/不调节)</div>
|
||
<el-slider v-model="scoreForm.score" :min="-20" :max="20" show-input
|
||
:marks="{'-20':'-20','-10':'-10','0':'0','10':'+10','20':'+20'}" />
|
||
|
||
<div class="preview-total">
|
||
预估总分 =
|
||
<span class="calc">{{ scoreForm.baseScore }} - {{ scoreForm.deduction }} + {{ scoreForm.reward || 0 }} + {{ scoreForm.score }}</span>
|
||
=
|
||
<b :class="totalClass(previewTotal)">{{ previewTotal }}</b>
|
||
({{ gradeOf(previewTotal) }})
|
||
</div>
|
||
</div>
|
||
<div slot="footer">
|
||
<el-button @click="scoreOpen = false">取消</el-button>
|
||
<el-button type="primary" :loading="scoreSubmitting" @click="submitScore">保存打分</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 流水弹窗 -->
|
||
<el-dialog title="扣分 / 加分 流水" :visible.sync="detailOpen" width="700px" append-to-body>
|
||
<div v-if="detail.user" class="detail-head">
|
||
<span class="u-name">{{ detail.user }}</span>
|
||
<span class="u-period">{{ query.period }}</span>
|
||
</div>
|
||
<el-empty v-if="!detail.list.length" :image-size="60" description="无流水" />
|
||
<el-table v-else :data="detail.list" size="small" border>
|
||
<el-table-column label="时间" prop="createTime" width="160" />
|
||
<el-table-column label="来源" width="120" align="center">
|
||
<template slot-scope="{row}">
|
||
<el-tag size="mini" :type="srcTag(row.source)">{{ srcLabel(row.source) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="业务" width="80" align="center">
|
||
<template slot-scope="{row}">{{ bizLabel(row.sourceType) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="分值" width="70" align="center">
|
||
<template slot-scope="{row}">
|
||
<b :style="{color: row.points > 0 ? '#f56c6c' : '#67c23a'}">
|
||
{{ row.points > 0 ? '-' : '+' }}{{ Math.abs(row.points) }}
|
||
</b>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="原因" prop="reason" show-overflow-tooltip />
|
||
</el-table>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { rankPerformance, userPerformance, setSubjective, manualAdjust } from '@/api/oa/performance'
|
||
|
||
// 可以打分的人员白名单:高总 + 陆永强
|
||
const SCORER_USER_IDS = ['1859252208375152641', '1858417253738815490']
|
||
|
||
export default {
|
||
name: 'OaPerformanceRank',
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
query: { period: '', nameLike: '' },
|
||
filter: 'all',
|
||
list: [],
|
||
detailOpen: false,
|
||
detail: { user: '', list: [] },
|
||
scoreOpen: false,
|
||
scoreSubmitting: false,
|
||
scoreForm: { userId: null, userName: '', baseScore: 80, deduction: 0, reward: 0, score: 0 },
|
||
adjustOpen: false,
|
||
adjustSubmitting: false,
|
||
adjustForm: { userId: null, userName: '', dir: -1, absPoints: 5, reason: '' },
|
||
adjustRules: {
|
||
reason: [{ required: true, message: '请填写原因(必填)', trigger: 'blur' },
|
||
{ min: 3, message: '原因至少 3 个字', trigger: 'blur' }]
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
canScore() {
|
||
const uid = this.$store.getters.userId || (this.$store.state.user && this.$store.state.user.userId)
|
||
const uidStr = String(uid || '')
|
||
const roles = (this.$store.state.user && this.$store.state.user.roles) || []
|
||
return SCORER_USER_IDS.includes(uidStr)
|
||
|| roles.includes('admin')
|
||
|| (this.$store.state.user && this.$store.state.user.name === 'admin')
|
||
},
|
||
currentUserName() {
|
||
return (this.$store.state.user && (this.$store.state.user.nickName || this.$store.state.user.name)) || '当前操作人'
|
||
},
|
||
filteredList() {
|
||
if (this.filter === 'all') return this.list
|
||
return this.list.filter(r => r.grade && r.grade.charAt(0) === this.filter)
|
||
},
|
||
podium() {
|
||
return this.filteredList.slice(0, 3)
|
||
},
|
||
restList() {
|
||
return this.filteredList.slice(3)
|
||
},
|
||
previewTotal() {
|
||
const t = (this.scoreForm.baseScore || 0) - (this.scoreForm.deduction || 0)
|
||
+ (this.scoreForm.reward || 0) + (this.scoreForm.score || 0)
|
||
return Math.max(0, Math.min(100, t))
|
||
}
|
||
},
|
||
created() {
|
||
const d = new Date()
|
||
this.query.period = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0')
|
||
this.load()
|
||
},
|
||
methods: {
|
||
load() {
|
||
this.loading = true
|
||
rankPerformance(this.query).then(res => {
|
||
this.list = res.data || []
|
||
}).finally(() => { this.loading = false })
|
||
},
|
||
countByLetter(letter) {
|
||
return this.list.filter(r => r.grade && r.grade.charAt(0) === letter).length
|
||
},
|
||
indexFn(i) { return i + 4 },
|
||
cardClass(g) {
|
||
if (!g) return ''
|
||
if (g.startsWith('A')) return 'card-a'
|
||
if (g.startsWith('B')) return 'card-b'
|
||
if (g.startsWith('C')) return 'card-c'
|
||
return 'card-d'
|
||
},
|
||
gradeClass(g) {
|
||
if (!g) return ''
|
||
if (g.startsWith('A')) return 'g-a'
|
||
if (g.startsWith('B')) return 'g-b'
|
||
if (g.startsWith('C')) return 'g-c'
|
||
return 'g-d'
|
||
},
|
||
gradeDot(g) { return this.gradeClass(g) },
|
||
rowClass({ row }) {
|
||
if (!row.grade) return ''
|
||
if (row.grade.startsWith('D')) return 'row-d'
|
||
return ''
|
||
},
|
||
totalClass(n) {
|
||
n = Number(n) || 0
|
||
if (n >= 90) return 'ok'
|
||
if (n >= 75) return 'warn'
|
||
return 'bad'
|
||
},
|
||
gradeOf(n) {
|
||
if (n >= 95) return 'A+'; if (n >= 90) return 'A'; if (n >= 85) return 'A-'
|
||
if (n >= 80) return 'B+'; if (n >= 75) return 'B'; if (n >= 70) return 'B-'
|
||
if (n >= 65) return 'C+'; if (n >= 60) return 'C'; if (n >= 55) return 'C-'
|
||
return 'D'
|
||
},
|
||
openDetail(row) {
|
||
userPerformance(row.user_id, this.query.period).then(res => {
|
||
const d = res.data || {}
|
||
this.detail = { user: row.user_nick, list: d.deductions || [] }
|
||
this.detailOpen = true
|
||
})
|
||
},
|
||
openScore(row) {
|
||
this.scoreForm = {
|
||
userId: row.user_id, userName: row.user_nick,
|
||
baseScore: row.base_score || 80,
|
||
deduction: row.deduction || 0,
|
||
reward: row.reward || 0,
|
||
score: row.bonus == null ? 0 : row.bonus
|
||
}
|
||
this.scoreOpen = true
|
||
},
|
||
submitScore() {
|
||
this.scoreSubmitting = true
|
||
setSubjective(this.scoreForm.userId, this.scoreForm.userName, this.query.period, this.scoreForm.score)
|
||
.then(() => {
|
||
this.$modal.msgSuccess('已保存')
|
||
this.scoreOpen = false
|
||
this.load()
|
||
}).finally(() => { this.scoreSubmitting = false })
|
||
},
|
||
openAdjust(row) {
|
||
this.adjustForm = {
|
||
userId: row.user_id, userName: row.user_nick,
|
||
dir: -1, absPoints: 5, reason: ''
|
||
}
|
||
this.adjustOpen = true
|
||
},
|
||
submitAdjust() {
|
||
this.$refs.adjustFormRef.validate(valid => {
|
||
if (!valid) {
|
||
this.$modal.msgWarning('请填写原因(必填)')
|
||
return
|
||
}
|
||
this.adjustSubmitting = true
|
||
const points = this.adjustForm.dir * this.adjustForm.absPoints
|
||
const fullReason = '[' + this.currentUserName + '] ' + this.adjustForm.reason.trim()
|
||
manualAdjust(this.adjustForm.userId, this.adjustForm.userName, points, fullReason)
|
||
.then(() => {
|
||
this.$modal.msgSuccess(points > 0 ? '已扣分' : '已加分')
|
||
this.adjustOpen = false
|
||
this.load()
|
||
}).finally(() => { this.adjustSubmitting = false })
|
||
})
|
||
},
|
||
srcLabel(s) { return ({ postpone:'顺延', overdue30:'超期30天', overdue_daily:'日逾期', noreport:'忘报工', manual:'手动', manual_subjective:'高总打分', reward_task:'完成任务', reward_step:'完成步骤', reward_report:'报工', reward_trip:'出差', reward_attend:'全勤' })[s] || s },
|
||
srcTag(s) {
|
||
if (s && s.startsWith('reward_')) return 'success'
|
||
if (s === 'manual') return 'info'
|
||
return 'danger'
|
||
},
|
||
bizLabel(t) { return ({ task:'任务', step:'步骤', requirement:'采购需求' })[t] || (t || '-') }
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.rank {
|
||
padding: 16px 20px; background: #f5f7fa; min-height: calc(100vh - 50px);
|
||
}
|
||
.muted { color: #909399; font-size: 12px; }
|
||
.ok { color: #67c23a; }
|
||
.warn { color: #e6a23c; }
|
||
.bad { color: #f56c6c; }
|
||
.of { color: #909399; font-size: 11px; margin-left: 2px; }
|
||
|
||
/* ====== Toolbar ====== */
|
||
.toolbar {
|
||
background: #fff; padding: 12px 16px; border-radius: 10px; margin-bottom: 14px;
|
||
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.left { display: flex; gap: 10px; align-items: center; }
|
||
.filter-tabs { display: flex; gap: 4px; background: #f4f4f5; padding: 4px; border-radius: 18px; }
|
||
.tab {
|
||
padding: 4px 14px; border-radius: 14px; font-size: 12px; cursor: pointer;
|
||
color: #606266; transition: all .15s; user-select: none;
|
||
}
|
||
.tab.active { background: #fff; color: #303133; box-shadow: 0 1px 3px rgba(0,0,0,.1); font-weight: 600; }
|
||
.tab.ok.active { color: #67c23a; }
|
||
.tab.b.active { color: #409EFF; }
|
||
.tab.c.active { color: #e6a23c; }
|
||
.tab.d.active { color: #f56c6c; }
|
||
|
||
/* ====== Podium 前三名 ====== */
|
||
.podium {
|
||
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; margin-bottom: 14px;
|
||
}
|
||
.podium-slot { position: relative; }
|
||
.podium-slot.p-1 { order: 2; } /* 第一名居中 */
|
||
.podium-slot.p-2 { order: 1; }
|
||
.podium-slot.p-3 { order: 3; }
|
||
.podium-card {
|
||
background: #fff; border-radius: 14px; padding: 22px 18px 16px; text-align: center;
|
||
position: relative; box-shadow: 0 2px 10px rgba(0,0,0,.04);
|
||
transition: all .2s;
|
||
margin-top: 30px;
|
||
}
|
||
.podium-slot.p-1 .podium-card { margin-top: 0; padding: 30px 18px 18px; }
|
||
.podium-card:hover { box-shadow: 0 8px 24px rgba(0,0,0,.1); transform: translateY(-3px); }
|
||
.podium-card.card-a { background: linear-gradient(180deg, #f0f9eb 0%, #fff 50%); border-top: 4px solid #67c23a; }
|
||
.podium-card.card-b { background: linear-gradient(180deg, #ecf5ff 0%, #fff 50%); border-top: 4px solid #409EFF; }
|
||
.podium-card.card-c { background: linear-gradient(180deg, #fdf6ec 0%, #fff 50%); border-top: 4px solid #e6a23c; }
|
||
.podium-card.card-d { background: linear-gradient(180deg, #fef0f0 0%, #fff 50%); border-top: 4px solid #f56c6c; }
|
||
|
||
.medal {
|
||
position: absolute; top: -20px; left: 50%; transform: translateX(-50%);
|
||
z-index: 2; display: flex; align-items: center; gap: 4px;
|
||
padding: 6px 14px; border-radius: 20px; color: #fff;
|
||
font-size: 12px; font-weight: 600;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,.15);
|
||
}
|
||
.p-1 .medal { background: linear-gradient(135deg, #ffd700, #ff9500); top: -16px; }
|
||
.p-2 .medal { background: linear-gradient(135deg, #d0d4d8, #909399); }
|
||
.p-3 .medal { background: linear-gradient(135deg, #cd7f32, #a0522d); }
|
||
.medal i { font-size: 14px; }
|
||
|
||
.p-grade {
|
||
font-size: 60px; font-weight: 800; line-height: 1; margin: 6px 0 10px;
|
||
color: #67c23a;
|
||
}
|
||
.podium-slot.p-1 .p-grade { font-size: 72px; }
|
||
.p-grade.g-a { color: #67c23a; }
|
||
.p-grade.g-b { color: #409EFF; }
|
||
.p-grade.g-c { color: #e6a23c; }
|
||
.p-grade.g-d { color: #f56c6c; }
|
||
|
||
.p-name { font-size: 15px; font-weight: 600; color: #303133; margin-bottom: 6px; }
|
||
.p-score { display: flex; justify-content: center; align-items: baseline; gap: 3px; margin-bottom: 10px; }
|
||
.p-score .num { font-size: 26px; font-weight: 700; }
|
||
.p-score .num.ok { color: #67c23a; }
|
||
.p-score .num.warn { color: #e6a23c; }
|
||
.p-score .num.bad { color: #f56c6c; }
|
||
.p-score .div { font-size: 12px; color: #909399; }
|
||
.p-mini {
|
||
display: flex; justify-content: center; gap: 8px; font-size: 11px;
|
||
color: #606266; padding: 8px 0; border-top: 1px dashed #ebeef5;
|
||
border-bottom: 1px dashed #ebeef5; margin-bottom: 10px;
|
||
}
|
||
.p-actions { display: flex; gap: 6px; justify-content: center; margin-bottom: 8px; }
|
||
|
||
/* ====== Rest Table ====== */
|
||
.rest { background: #fff; border-radius: 10px; padding: 10px 14px; }
|
||
.user-cell { display: flex; align-items: center; gap: 8px; }
|
||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: #c0c4cc; }
|
||
.dot.g-a { background: #67c23a; }
|
||
.dot.g-b { background: #409EFF; }
|
||
.dot.g-c { background: #e6a23c; }
|
||
.dot.g-d { background: #f56c6c; }
|
||
|
||
.grade-tag {
|
||
display: inline-block; padding: 2px 12px; border-radius: 12px;
|
||
color: #fff; font-weight: 600; font-size: 12px;
|
||
}
|
||
.grade-tag.g-a { background: #67c23a; }
|
||
.grade-tag.g-b { background: #409EFF; }
|
||
.grade-tag.g-c { background: #e6a23c; }
|
||
.grade-tag.g-d { background: #f56c6c; }
|
||
|
||
::v-deep .row-d { background: #fff5f5 !important; }
|
||
|
||
/* ====== Dialogs ====== */
|
||
.form-body { padding: 0 8px; }
|
||
.form-user {
|
||
display: flex; align-items: baseline; gap: 12px; margin-bottom: 18px;
|
||
padding-bottom: 14px; border-bottom: 1px dashed #ebeef5;
|
||
}
|
||
.form-user .u-name { font-size: 18px; font-weight: 600; color: #303133; }
|
||
.form-user .u-period { color: #909399; font-size: 13px; }
|
||
.preview { margin-left: 14px; font-size: 15px; font-weight: 600; }
|
||
.op-name { color: #606266; }
|
||
|
||
.info-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 18px; }
|
||
.info-tile { background: #fafafa; border-radius: 6px; padding: 10px; text-align: center; }
|
||
.i-lbl { font-size: 11px; color: #909399; margin-bottom: 3px; }
|
||
.i-val { font-size: 20px; font-weight: 700; color: #303133; }
|
||
.i-val.ok { color: #67c23a; }
|
||
.i-val.bad { color: #f56c6c; }
|
||
|
||
.slider-label { color: #303133; font-weight: 500; margin-bottom: 14px; font-size: 13px; }
|
||
.preview-total {
|
||
margin-top: 24px; padding: 12px 16px; background: #fafafa;
|
||
border-radius: 8px; font-size: 13px; color: #606266; line-height: 1.7;
|
||
}
|
||
.preview-total .calc { color: #909399; font-family: Menlo, Consolas, monospace; }
|
||
.preview-total b { font-size: 22px; margin: 0 4px; }
|
||
.preview-total b.ok { color: #67c23a; }
|
||
.preview-total b.warn { color: #e6a23c; }
|
||
.preview-total b.bad { color: #f56c6c; }
|
||
|
||
.detail-head { display: flex; gap: 14px; align-items: baseline; margin-bottom: 14px; padding-bottom: 8px; border-bottom: 1px dashed #ebeef5; }
|
||
.detail-head .u-name { font-size: 16px; font-weight: 600; }
|
||
.detail-head .u-period { color: #909399; }
|
||
</style>
|