Files
kra-new/web/src/view/sw/components/GoalDrawer.vue
T

56 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>