feat: 完善小组绩效与有效端统计并修复审查问题

This commit is contained in:
Yvan
2026-11-05 01:10:03 +08:00
parent ca9ce99b66
commit dc97690eb1
75 changed files with 5033 additions and 296 deletions
+51
View File
@@ -0,0 +1,51 @@
<template>
<el-drawer v-model="visible" append-to-body :title="`设置目标 · ${target?.name || data?.name || ''}`" size="min(650px, 96vw)" destroy-on-close>
<div v-loading="loading">
<el-alert v-if="error" type="error" :title="error" :closable="false" class="mb-3" />
<el-tabs v-model="tab"><el-tab-pane label="设置目标" name="settings" /><el-tab-pane label="目标记录" name="history" /></el-tabs>
<template v-if="tab === 'settings'">
<el-form label-position="top" :disabled="saving || !data?.canEdit">
<el-form-item label="月份"><el-select v-model="month" @change="fill"><el-option v-for="m in monthChoices" :key="m" :value="m" :label="`${m}${m === currentMonth() ? '(本月)' : ''}`" /></el-select></el-form-item>
<el-text v-if="target?.kind === 'group'" tag="p" class="mb-4">当前团队绩效:开端 {{ selected?.actual.open || 0 }} · 有效端 {{ selected?.actual.effective || 0 }} · {{ displayMetric('pay', selected?.actual.pay || '0') }}</el-text>
<el-form-item v-for="metric in metrics" :key="metric.key" :label="`${metric.label}(${metric.key === 'pay' ? '元' : '个'})`">
<el-input v-model="form[metric.goal]" inputmode="numeric" clearable placeholder="留空表示不考核" />
<el-text size="small" type="info" class="mt-2">{{ target?.kind === 'group' ? '当前实际' : '上月目标' }}:{{ target?.kind === 'group' ? displayMetric(metric.key, selected?.actual[metric.key]) : displayTarget(metric.goal, previous?.goal?.[metric.goal]) }}</el-text>
</el-form-item>
</el-form>
<el-text v-if="target?.kind === 'group'" tag="p" type="info">参考:成员个人目标合计 {{ data?.memberGoals?.[month]?.open || 0 }} / {{ data?.memberGoals?.[month]?.effective || 0 }} / {{ displayMetric('pay', data?.memberGoals?.[month]?.pay || '0') }},不会自动替代小组目标。</el-text>
<el-text tag="p" type="info" class="mt-3">实际绩效自动统计;个人包含本人及全部下级,小组为组内成员绩效之和。留空表示该项不考核,三项全空表示未设目标。</el-text>
<el-text v-if="selected?.goal?.updatedBy" tag="p" type="info" size="small" class="mt-3">最后修改人:#{{ selected.goal.updatedBy }} · {{ selected.goal.updatedAt }}</el-text>
</template>
<el-table v-else :data="history" size="small">
<el-table-column prop="month" label="月份" width="85" />
<el-table-column v-for="metric in metrics" :key="metric.key" :label="metric.label" min-width="120"><template #default="{ row }"><span v-if="row.goal?.[metric.goal] != null">{{ displayTarget(metric.goal, row.goal[metric.goal]) }} / {{ row.available === false ? '未接入' : displayMetric(metric.key, row.actual[metric.key]) }}</span><span v-else>—</span></template></el-table-column>
<el-table-column label="达成" width="95"><template #default="{ row }"><el-tag :type="statusType(row.status)">{{ row.status === 'missing' ? `差 ${row.missing} 项` : statuses[row.status] }}</el-tag></template></el-table-column>
<el-table-column width="60"><template #default="{ row }"><el-button v-if="row.canEdit" link type="primary" @click="editMonth(row.month)">编辑</el-button></template></el-table-column>
</el-table>
</div>
<template #footer><div v-if="tab === 'settings'"><el-button :disabled="saving || !data?.canEdit" @click="clear">清空本月目标</el-button><el-button type="primary" :loading="saving" :disabled="loading || !data?.canEdit" @click="save">保存</el-button></div></template>
</el-drawer>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getGoals, saveGoal } from '@/api/performance'
import { performanceMetrics as metrics, currentMonth, monthOffset, displayMetric, displayTarget } from '../performance-display'
const props = defineProps({ modelValue: Boolean, target: Object }), emit = defineEmits(['update:modelValue', 'saved'])
const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) })
const loading = ref(false), saving = ref(false), error = ref(''), data = ref(null), tab = ref('settings'), month = ref(currentMonth())
const form = reactive({ open: '', effective: '', amount: '' })
let version = 0
const selected = computed(() => data.value?.months.find(m => m.month === month.value)), previous = computed(() => data.value?.months.find(m => m.month === monthOffset(month.value, -1)))
const history = computed(() => (data.value?.months || []).filter(m => m.month <= currentMonth()).slice(0, 6))
const monthChoices = computed(() => [...new Set([month.value, ...[-1, 0, 1].map(n => monthOffset(currentMonth(), n))])].sort())
const statuses = { unset: '未设目标', ongoing: '进行中', future: '未开始', achieved: '达成', unavailable: '未接入' }
const statusType = status => ({ achieved: 'success', missing: 'danger', ongoing: 'primary' }[status] || 'info')
const fill = () => { for (const key of Object.keys(form)) form[key] = selected.value?.goal?.[key] == null ? '' : String(selected.value.goal[key]) }
const load = async () => { const current = ++version; loading.value = true; error.value = ''; try { const res = await getGoals({ kind: props.target.kind, targetId: props.target.id }); if (current !== version) return; if (res.code !== 0) { error.value = res.msg || '目标读取失败'; return } data.value = res.data; fill() } catch { if (current === version) error.value = '目标读取失败,请重试' } finally { if (current === version) loading.value = false } }
watch(visible, v => { if (v) { data.value = null; tab.value = 'settings'; month.value = currentMonth(); load() } else version++ })
const editMonth = value => { month.value = value; fill(); tab.value = 'settings' }
const persist = async values => { saving.value = true; error.value = ''; try { const res = await saveGoal({ kind: props.target.kind, targetId: props.target.id, month: month.value, ...values }); if (res.code !== 0) { error.value = res.msg || '目标保存失败'; return } ElMessage.success('目标已保存'); emit('saved'); await load() } catch { error.value = '目标保存失败,请重试' } finally { saving.value = false } }
const save = async () => { if (saving.value) return; const values = {}; for (const [key, raw] of Object.entries(form)) { if (raw.trim() === '') { values[key] = null; continue } if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(Number(raw))) { ElMessage.warning('目标须为非负整数,或留空'); return } values[key] = Number(raw) } await persist(values) }
const clear = async () => { if (saving.value) return; try { await ElMessageBox.confirm(`确认清空 ${month.value} 的全部目标?`, '清空目标', { type: 'warning' }) } catch { return } await persist({ open: null, effective: null, amount: null }) }
</script>