56 lines
7.2 KiB
Vue
56 lines
7.2 KiB
Vue
<template>
|
||
<el-drawer class="sw-dialog" 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 scrollbar-always-on 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="150"><template #default="{ row }">{{ displayTarget(metric.goal, row.goal?.[metric.goal]) }} / {{ row.available === false ? '未接入' : displayMetric(metric.key, row.actual[metric.key]) }}</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, onBeforeUnmount, onDeactivated, reactive, ref, watch } from 'vue'
|
||
import { createLatestRequest } from '@/utils/latestRequest'
|
||
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: '' })
|
||
const requests = createLatestRequest()
|
||
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 () => { if (!visible.value || !props.target) return; const request = requests.start(); loading.value = true; error.value = ''; try { const res = await getGoals({ kind: props.target.kind, targetId: props.target.id }, { signal: request.signal }); if (!request.isCurrent()) return; if (res.code !== 0) { error.value = res.msg || '目标读取失败'; return } data.value = res.data; fill() } catch { if (request.isCurrent()) error.value = '目标读取失败,请重试' } finally { if (request.isCurrent()) loading.value = false } }
|
||
const cancel = () => { requests.cancel(); loading.value = false }
|
||
watch(() => [props.modelValue, props.target], ([open]) => { if (open) { data.value = null; tab.value = 'settings'; month.value = currentMonth(); load() } else cancel() }, { flush: 'sync' })
|
||
onBeforeUnmount(cancel)
|
||
onDeactivated(() => { visible.value = false; cancel() })
|
||
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>
|