添加了项目前景 绩效 审批配置做了一部分有点晕 我换换脑子继续这个 还有说明菜单
This commit is contained in:
193
ruoyi-ui/src/components/PerfWidget/index.vue
Normal file
193
ruoyi-ui/src/components/PerfWidget/index.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div class="perf-w" v-loading="loading" @click="goDetail">
|
||||
<div v-if="score" class="card" :class="lvClass">
|
||||
<!-- 顶部:等级 + 分数 -->
|
||||
<div class="row1">
|
||||
<div class="grade">{{ score.grade || '-' }}</div>
|
||||
<div class="right">
|
||||
<div class="score">
|
||||
<span class="num">{{ score.totalScore || 0 }}</span>
|
||||
<span class="div">/100</span>
|
||||
</div>
|
||||
<div class="period">{{ score.period }} 月度绩效</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:渐变进度条 -->
|
||||
<div class="track">
|
||||
<div class="track-fill" :style="{ width: pct(score.totalScore, 100) }"></div>
|
||||
</div>
|
||||
|
||||
<!-- 底部:mini 指标 -->
|
||||
<div class="row2">
|
||||
<div class="pill">
|
||||
<span class="lbl">基础</span>
|
||||
<span class="val">{{ score.baseScore || 80 }}</span>
|
||||
</div>
|
||||
<div class="pill" v-if="(score.reward || 0) > 0">
|
||||
<span class="lbl">加</span>
|
||||
<span class="val ok">+{{ score.reward }}</span>
|
||||
</div>
|
||||
<div class="pill" v-if="(score.deduction || 0) > 0">
|
||||
<span class="lbl">扣</span>
|
||||
<span class="val bad">-{{ score.deduction }}</span>
|
||||
</div>
|
||||
<div class="pill">
|
||||
<span class="lbl">主观</span>
|
||||
<span class="val">{{ score.bonus || 0 }}</span>
|
||||
<span v-if="!isSet" class="tip">·待打</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow"><i class="el-icon-arrow-right"></i></div>
|
||||
</div>
|
||||
<div v-else-if="!loading" class="empty">暂无绩效数据</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { myPerformance } from '@/api/oa/performance'
|
||||
|
||||
export default {
|
||||
name: 'PerfWidget',
|
||||
data() { return { loading: false, score: null } },
|
||||
computed: {
|
||||
lvClass() {
|
||||
const g = this.score && this.score.grade
|
||||
if (!g) return ''
|
||||
if (g.startsWith('A')) return 'lv-a'
|
||||
if (g.startsWith('B')) return 'lv-b'
|
||||
if (g.startsWith('C')) return 'lv-c'
|
||||
return 'lv-d'
|
||||
},
|
||||
isSet() {
|
||||
return this.score && (this.score.subjectiveSet === 1 || this.score.subjective_set === 1)
|
||||
}
|
||||
},
|
||||
created() { this.load() },
|
||||
methods: {
|
||||
load() {
|
||||
this.loading = true
|
||||
myPerformance().then(res => {
|
||||
this.score = (res.data && res.data.score) || null
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
pct(v, max) {
|
||||
if (!max) return '0%'
|
||||
return Math.max(0, Math.min(100, (v / max) * 100)) + '%'
|
||||
},
|
||||
goDetail() {
|
||||
// 从用户路由树里找到"我的绩效"组件对应的实际路径,避免菜单被挪窝后写死失效
|
||||
const target = 'oa/performance/mine/index'
|
||||
const routes = this.$store.getters && this.$store.getters.permission_routes || this.$router.options.routes || []
|
||||
const path = this.findRoutePath(routes, target, '')
|
||||
if (path) {
|
||||
this.$router.push(path)
|
||||
} else {
|
||||
// 兜底:还是按默认 /perf/mine 尝试一下
|
||||
this.$router.push('/perf/mine').catch(() => { window.location.assign('/perf/mine') })
|
||||
}
|
||||
},
|
||||
findRoutePath(routes, target, parent) {
|
||||
for (const r of routes) {
|
||||
let p = r.path || ''
|
||||
if (p && !p.startsWith('/')) p = (parent.endsWith('/') ? parent : parent + '/') + p
|
||||
else if (p) p = p
|
||||
else p = parent
|
||||
const cmp = r.component
|
||||
const cmpStr = cmp && (cmp.name || (cmp.toString && cmp.toString().match(/['"]([^'"]*?)['"]/)?.[1])) || ''
|
||||
if (r.meta && r.meta.title === '我的绩效') return p
|
||||
if (typeof cmpStr === 'string' && cmpStr.indexOf('performance/mine') >= 0) return p
|
||||
if (r.children && r.children.length) {
|
||||
const found = this.findRoutePath(r.children, target, p)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.perf-w {
|
||||
height: 100%; padding: 6px; box-sizing: border-box; cursor: pointer;
|
||||
}
|
||||
.card {
|
||||
height: 100%; position: relative;
|
||||
background: linear-gradient(135deg, #fff 0%, #f5fbef 100%);
|
||||
border-radius: 12px; padding: 14px 16px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.04);
|
||||
transition: all .2s; overflow: hidden;
|
||||
display: flex; flex-direction: column; justify-content: space-between;
|
||||
}
|
||||
.card:hover { box-shadow: 0 6px 18px rgba(0,0,0,.08); transform: translateY(-1px); }
|
||||
.card::before {
|
||||
content: ''; position: absolute; top: 0; right: 0; width: 130px; height: 130px;
|
||||
background: radial-gradient(circle, rgba(103,194,58,0.18) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.card.lv-a { background: linear-gradient(135deg, #fff 0%, #f5fbef 100%); }
|
||||
.card.lv-b { background: linear-gradient(135deg, #fff 0%, #ecf5ff 100%); }
|
||||
.card.lv-b::before { background: radial-gradient(circle, rgba(64,158,255,0.18) 0%, transparent 70%); }
|
||||
.card.lv-c { background: linear-gradient(135deg, #fff 0%, #fdf6ec 100%); }
|
||||
.card.lv-c::before { background: radial-gradient(circle, rgba(230,162,60,0.18) 0%, transparent 70%); }
|
||||
.card.lv-d { background: linear-gradient(135deg, #fff 0%, #fef0f0 100%); }
|
||||
.card.lv-d::before { background: radial-gradient(circle, rgba(245,108,108,0.18) 0%, transparent 70%); }
|
||||
|
||||
.row1 { display: flex; align-items: center; gap: 14px; position: relative; z-index: 1; }
|
||||
.grade {
|
||||
font-size: 56px; font-weight: 800; line-height: 1;
|
||||
color: #67c23a; min-width: 70px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
.card.lv-a .grade { color: #67c23a; }
|
||||
.card.lv-b .grade { color: #409EFF; }
|
||||
.card.lv-c .grade { color: #e6a23c; }
|
||||
.card.lv-d .grade { color: #f56c6c; }
|
||||
|
||||
.right { flex: 1; }
|
||||
.score { display: flex; align-items: baseline; gap: 4px; }
|
||||
.score .num { font-size: 28px; font-weight: 700; color: #303133; line-height: 1; }
|
||||
.score .div { font-size: 13px; color: #909399; }
|
||||
.period { font-size: 11px; color: #909399; margin-top: 2px; }
|
||||
|
||||
.track {
|
||||
margin: 10px 0 8px; height: 6px; background: #f0f0f0; border-radius: 3px; overflow: hidden;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
.track-fill {
|
||||
height: 100%; border-radius: 3px;
|
||||
background: linear-gradient(90deg, #67c23a 0%, #5daf34 100%);
|
||||
transition: width .6s ease;
|
||||
}
|
||||
.card.lv-b .track-fill { background: linear-gradient(90deg, #409EFF 0%, #337ecc 100%); }
|
||||
.card.lv-c .track-fill { background: linear-gradient(90deg, #e6a23c 0%, #cf9236 100%); }
|
||||
.card.lv-d .track-fill { background: linear-gradient(90deg, #f56c6c 0%, #d65b5b 100%); }
|
||||
|
||||
.row2 {
|
||||
display: flex; gap: 6px; flex-wrap: wrap; position: relative; z-index: 1;
|
||||
}
|
||||
.pill {
|
||||
background: rgba(255,255,255,0.7); border: 1px solid rgba(0,0,0,0.05);
|
||||
border-radius: 12px; padding: 2px 9px; font-size: 11px;
|
||||
display: flex; align-items: center; gap: 4px; color: #606266;
|
||||
}
|
||||
.pill .lbl { color: #909399; }
|
||||
.pill .val { font-weight: 600; color: #303133; font-family: Menlo, Consolas, monospace; }
|
||||
.pill .val.ok { color: #67c23a; }
|
||||
.pill .val.bad { color: #f56c6c; }
|
||||
.pill .tip { color: #c0c4cc; font-size: 10px; }
|
||||
|
||||
.arrow {
|
||||
position: absolute; bottom: 8px; right: 10px;
|
||||
color: #c0c4cc; font-size: 16px; z-index: 1;
|
||||
}
|
||||
.card:hover .arrow { color: #909399; transform: translateX(3px); transition: all .2s; }
|
||||
|
||||
.empty {
|
||||
color: #909399; font-size: 13px;
|
||||
height: 100%; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
import Announcements from '@/components/Announcements/index.vue'
|
||||
import MiniCalendar from '@/components/MiniCalendar/index.vue'
|
||||
import QuickEntry from '@/components/QuickEntry/index.vue'
|
||||
import PerfWidget from '@/components/PerfWidget/index.vue'
|
||||
import {
|
||||
ExpressQuestionList,
|
||||
FeedbackList,
|
||||
@@ -77,6 +78,11 @@ export const WIDGET_REGISTRY = {
|
||||
title: '快捷入口',
|
||||
component: QuickEntry,
|
||||
defaultSize: { w: 12, h: 4 }
|
||||
},
|
||||
perf: {
|
||||
title: '我的绩效',
|
||||
component: PerfWidget,
|
||||
defaultSize: { w: 4, h: 4 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user