performance and salary

This commit is contained in:
何德超 2025-07-17 21:51:51 +08:00
parent 71b3d890cb
commit e608ebf4d2
10 changed files with 661 additions and 0 deletions

View File

@ -0,0 +1,75 @@
<template>
<a-drawer
v-model:visible="visible"
:title="isUpdate ? '修改维度' : '新增维度'"
@before-ok="save"
@cancel="reset"
>
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="维度名称" field="name" required>
<a-input v-model="form.name" />
</a-form-item>
<a-form-item label="权重%" field="weight" required>
<a-input-number v-model="form.weight" :min="1" :max="100" />
</a-form-item>
<a-form-item label="描述" field="desc">
<a-textarea v-model="form.desc" :rows="3" />
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch v-model="form.status" :checked-value="0" :unchecked-value="1" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { addDimension, getDimensionDetail, updateDimension } from '@/apis/performance'
const emit = defineEmits<{
(e: 'success'): void
}>()
const visible = ref(false)
const isUpdate = ref(false)
const formRef = ref()
const id = ref('')
const form = reactive({
name: '',
weight: 50,
desc: '',
status: 0 as 0 | 1,
})
const reset = () => {
formRef.value?.resetFields()
Object.assign(form, { name: '', weight: 50, desc: '', status: 0 })
id.value = ''
}
const save = async () => {
if (isUpdate.value) {
await updateDimension(id.value, form)
} else {
await addDimension(form)
}
visible.value = false
emit('success')
}
const onAdd = () => {
reset()
isUpdate.value = false
visible.value = true
}
const onUpdate = async (dimensionId: string) => {
reset()
isUpdate.value = true
id.value = dimensionId
const data = await getDimensionDetail(dimensionId)
Object.assign(form, data)
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>

View File

@ -0,0 +1,100 @@
<template>
<a-spin :loading="loading">
<a-descriptions :data="detailData" bordered>
<a-descriptions-item label="员工">
{{ data.userName }}
</a-descriptions-item>
<a-descriptions-item label="维度">
{{ data.dimensionName }}
</a-descriptions-item>
<a-descriptions-item label="周期">
{{ data.periodName }}
</a-descriptions-item>
<a-descriptions-item label="评分">
<a-tag color="blue" size="large">{{ data.score }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="AI评价" :span="2">
{{ data.aiComment }}
</a-descriptions-item>
</a-descriptions>
<!-- 反馈表单 -->
<a-divider orientation="left">评价反馈</a-divider>
<a-form ref="feedbackRef" :model="feedback" :rules="rules" layout="vertical">
<a-form-item label="是否有异议" field="level" required>
<a-radio-group v-model="feedback.level">
<a-radio :value="0">无异议</a-radio>
<a-radio :value="1">部分有异议</a-radio>
<a-radio :value="2">完全不同意</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="请说明理由" field="content" required>
<a-textarea
v-model="feedback.content"
:rows="4"
placeholder="请具体描述您对评价结果的看法..."
/>
</a-form-item>
<a-form-item>
<a-button type="primary" :loading="submitting" @click="handleSubmit">
提交反馈
</a-button>
</a-form-item>
</a-form>
</a-spin>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { submitFeedback } from '@/apis/performance'
import type { EvaluateResp, FeedbackReq } from '@/apis/performance/type'
interface Props {
data: EvaluateResp
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'feedback'): void
}>()
const loading = ref(false)
const submitting = ref(false)
const feedbackRef = ref()
const feedback = reactive<FeedbackReq>({
evaluateId: props.data.id,
level: 0,
content: '',
})
const rules = {
level: [{ required: true, message: '请选择异议等级' }],
content: [{ required: true, message: '请填写反馈内容' }],
}
const detailData = computed(() => [
{ label: '员工', value: props.data.userName },
{ label: '维度', value: props.data.dimensionName },
{ label: '周期', value: props.data.periodName },
{ label: '评分', value: props.data.score },
{ label: 'AI评价', value: props.data.aiComment },
])
const handleSubmit = async () => {
const ok = await feedbackRef.value?.validate()
if (ok) return
submitting.value = true
try {
await submitFeedback(feedback)
Message.success('反馈已提交')
emit('feedback')
} finally {
submitting.value = false
}
}
</script>

View File

@ -0,0 +1,18 @@
<template>
<a-menu
:default-selected-keys="['dimension']"
@menu-item-click="handleClick"
>
<a-menu-item key="dimension">绩效维度</a-menu-item>
<a-menu-item key="evaluate">员工评估</a-menu-item>
<a-menu-item key="my">我的绩效</a-menu-item>
</a-menu>
</template>
<script setup lang="ts">
const emit = defineEmits<{
(e: 'select', key: string): void
}>()
const handleClick = (key: string) => emit('select', key)
</script>

View File

@ -0,0 +1,131 @@
<template>
<a-drawer
v-model:visible="visible"
:title="isUpdate ? '编辑细则' : '新增细则'"
:width="480"
@before-ok="handleSave"
@cancel="reset"
>
<a-form ref="formRef" :model="form" auto-label-width>
<a-form-item label="所属维度" field="dimensionId" required>
<a-select v-model="form.dimensionId" placeholder="请选择维度">
<a-option
v-for="item in dimensionOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</a-select>
</a-form-item>
<a-form-item label="细则名称" field="name" required>
<a-input v-model="form.name" placeholder="如:持有基础资质证书" />
</a-form-item>
<a-form-item label="评分规则描述" field="ruleDesc" required>
<a-textarea
v-model="form.ruleDesc"
:rows="4"
placeholder="详细说明评分标准"
/>
</a-form-item>
<a-form-item label="满分" field="score" required>
<a-input-number v-model="form.score" :min="0" :precision="0" />
</a-form-item>
<a-form-item label="权重" field="weight" required>
<a-input-number v-model="form.weight" :min="0" :max="100" />
</a-form-item>
<a-form-item label="是否加分项" field="isExtra">
<a-switch v-model="form.isExtra" />
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
addRule,
getDimensionList,
getRuleDetail,
updateRule,
} from '@/apis/performance'
import type { DimensionResp, RuleAddReq, RuleUpdateReq } from '@/apis/performance/type'
const emit = defineEmits<{
(e: 'success'): void
}>()
const visible = ref(false)
const isUpdate = ref(false)
const id = ref('')
const formRef = ref()
const dimensionOptions = ref<DimensionResp[]>([])
const form = reactive<RuleAddReq>({
dimensionId: '',
name: '',
ruleDesc: '',
score: 100,
weight: 10,
isExtra: false,
})
const reset = () => {
formRef.value?.resetFields()
Object.assign(form, {
dimensionId: '',
name: '',
ruleDesc: '',
score: 100,
weight: 10,
isExtra: false,
})
id.value = ''
}
const openAdd = () => {
reset()
isUpdate.value = false
visible.value = true
}
const openUpdate = async (ruleId: string) => {
reset()
isUpdate.value = true
id.value = ruleId
const detail = await getRuleDetail(ruleId)
Object.assign(form, detail)
visible.value = true
}
const handleSave = async () => {
const ok = await formRef.value?.validate()
if (ok) return false
try {
if (isUpdate.value) {
await updateRule(id.value, form as RuleUpdateReq)
Message.success('细则已更新')
} else {
await addRule(form)
Message.success('细则已新增')
}
visible.value = false
emit('success')
return true
} catch {
return false
}
}
onMounted(async () => {
dimensionOptions.value = await getDimensionList()
})
defineExpose({ openAdd, openUpdate })
</script>

View File

@ -0,0 +1,67 @@
<template>
<a-modal
v-model:visible="visible"
title="细则管理"
:width="800"
@before-ok="handleSave"
@cancel="reset"
>
<a-table :data="rules" :columns="columns" row-key="id">
<template #action="{ record }">
<a-space>
<a-button type="text" @click="RuleDrawerRef?.openUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
</a-space>
</template>
</a-table>
<a-button type="primary" @click="RuleDrawerRef?.openAdd()">
<icon-plus /> 新增细则
</a-button>
</a-modal>
<RuleDrawer ref="RuleDrawerRef" @success="load" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import RuleDrawer from './RuleDrawer.vue'
import { deleteRule, getRuleList } from '@/apis/performance'
import type { RuleResp } from '@/apis/performance/type'
import { Message } from '@arco-design/web-vue'
const visible = ref(false)
const rules = ref<RuleResp[]>([])
const RuleDrawerRef = ref()
const columns = [
{ title: '细则名称', dataIndex: 'name' },
{ title: '评分规则描述', dataIndex: 'ruleDesc' },
{ title: '满分', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '是否加分项', dataIndex: 'isExtra', render: ({ record }) => record.isExtra ? '是' : '否' },
{ title: '操作', slotName: 'action' },
]
const open = async (dimensionId: string) => {
visible.value = true
rules.value = await getRuleList(dimensionId)
}
const handleDelete = async (id: string) => {
await deleteRule(id)
Message.success('删除成功')
load()
}
const load = async () => {
//
rules.value = await getRuleList(dimensionId.value)
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,72 @@
<template>
<div>
<a-card title="绩效维度管理">
<template #extra>
<a-button type="primary" @click="DimensionDrawerRef?.onAdd()">
<icon-plus /> 添加维度
</a-button>
</template>
<a-table :data="dimensions" :columns="columns" row-key="id">
<template #status="{ record }">
<a-tag :color="record.status === 0 ? 'green' : 'red'">
{{ record.status === 0 ? '启用' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button type="text" @click="DimensionDrawerRef?.onUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
<a-button type="text" @click="openRuleList(record.id)">
管理细则
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 维度抽屉 -->
<DimensionDrawer ref="DimensionDrawerRef" @success="load" />
<!-- 细则列表 -->
<RuleList ref="RuleListRef" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getDimensionList, deleteDimension } from '@/apis/performance'
import DimensionDrawer from './components/DimensionDrawer.vue'
import RuleList from './components/RuleList.vue'
import type { DimensionResp } from '@/apis/performance/type'
const dimensions = ref<DimensionResp[]>([])
const DimensionDrawerRef = ref()
const RuleListRef = ref()
const columns = [
{ title: '维度名称', dataIndex: 'name' },
{ title: '权重', dataIndex: 'weight', render: ({ record }) => `${record.weight}%` },
{ title: '细则数量', dataIndex: 'ruleCount' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
const response=await getDimensionList()
dimensions.value = await getDimensionList().data||[]
}
onMounted(load)
const handleDelete = async (id: string) => {
await deleteDimension(id)
load()
}
const openRuleList = (dimensionId: string) => {
RuleListRef.value.open(dimensionId)
}
</script>

View File

@ -0,0 +1,70 @@
<template>
<a-card title="员工绩效评估">
<a-form layout="inline" style="margin-bottom: 16px">
<a-form-item label="绩效周期">
<a-select v-model="query.periodId" placeholder="请选择周期" allow-clear>
<a-option v-for="p in periods" :key="p.id" :value="p.id">{{ p.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item label="维度">
<a-select v-model="query.dimensionId" placeholder="请选择维度" allow-clear>
<a-option v-for="d in dimensions" :key="d.id" :value="d.id">{{ d.name }}</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-input v-model="query.keyword" placeholder="姓名/工号" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="load">查询</a-button>
</a-form-item>
</a-form>
<a-table :data="data" :columns="columns" row-key="id">
<template #action="{ record }">
<a-button type="text" @click="openEvaluate(record)">
开始评估
</a-button>
</template>
</a-table>
</a-card>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { getDimensionList, getEvaluatePage, getPeriodList, startEvaluate } from '@/apis/performance'
import type { EvaluateQuery, EvaluateResp } from '@/apis/performance/type'
const periods = ref([])
const dimensions = ref([])
const query = ref<EvaluateQuery>({})
const data = ref<EvaluateResp[]>([])
const columns = [
{ title: '员工', dataIndex: 'userName' },
{ title: '维度', dataIndex: 'dimensionName' },
{ title: '周期', dataIndex: 'periodName' },
{ title: '评分', dataIndex: 'score' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
data.value = await getEvaluatePage(query.value)
}
onMounted(async () => {
periods.value = await getPeriodList()
dimensions.value = await getDimensionList()
load()
})
const openEvaluate = async (row: EvaluateResp) => {
await startEvaluate({
userId: row.userId,
dimensionId: row.dimensionId,
ruleId: row.ruleId,
periodId: row.periodId,
})
Message.success('评估已启动')
load()
}
</script>

View File

@ -0,0 +1,19 @@
<template>
<GiPageLayout>
<template #left>
<PerformanceMenu @select="handleSelectMenu" />
</template>
<router-view />
</GiPageLayout>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import PerformanceMenu from './components/PerformanceMenu.vue'
const router = useRouter()
const handleSelectMenu = (key: string) => {
router.push(`/performance/${key}`)
}
</script>

View File

@ -0,0 +1,48 @@
<template>
<a-card title="我的绩效">
<a-table :data="data" :columns="columns" row-key="id">
<template #action="{ record }">
<a-button type="text" @click="openDetail(record)">查看详情</a-button>
</template>
</a-table>
</a-card>
<a-modal v-model:visible="visible" title="绩效详情">
<EvaluateDetail v-if="selected" :data="selected" @feedback="handleFeedback" />
</a-modal>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import EvaluateDetail from './components/EvaluateDetail.vue'
import { getMyEvaluate } from '@/apis/performance'
import type { EvaluateResp } from '@/apis/performance/type'
const data = ref<EvaluateResp[]>([])
const visible = ref(false)
const selected = ref<EvaluateResp>()
const columns = [
{ title: '维度', dataIndex: 'dimensionName' },
{ title: '周期', dataIndex: 'periodName' },
{ title: '评分', dataIndex: 'score' },
{ title: '操作', slotName: 'action' },
]
const load = async () => {
const response = await getMyEvaluate()
data.value = response.data?.list || []
}
const openDetail = (row: EvaluateResp) => {
selected.value = row
visible.value = true
}
const handleFeedback = () => {
visible.value = false
load()
}
onMounted(load)
</script>

View File

@ -0,0 +1,61 @@
<template>
<div>
<a-card title="绩效细则管理">
<template #extra>
<a-button type="primary" @click="RuleDrawerRef?.openAdd()">
<icon-plus /> 新增细则
</a-button>
</template>
<a-table :data="rules" :columns="columns" row-key="id">
<template #action="{ record }">
<a-space>
<a-button type="text" @click="RuleDrawerRef?.openUpdate(record.id)">
编辑
</a-button>
<a-button type="text" status="danger" @click="handleDelete(record.id)">
删除
</a-button>
</a-space>
</template>
</a-table>
</a-card>
<!-- 细则抽屉 -->
<RuleDrawer ref="RuleDrawerRef" @success="load" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import RuleDrawer from './components/RuleDrawer.vue'
import { deleteRule, getRuleList } from '@/apis/performance'
import type { RuleResp } from '@/apis/performance/type'
const rules = ref<RuleResp[]>([])
const RuleDrawerRef = ref()
const columns = [
{ title: '细则名称', dataIndex: 'name' },
{ title: '评分规则描述', dataIndex: 'ruleDesc' },
{ title: '满分', dataIndex: 'score' },
{ title: '权重', dataIndex: 'weight' },
{ title: '是否加分项', dataIndex: 'isExtra', render: ({ record }) => record.isExtra ? '是' : '否' },
{ title: '操作', slotName: 'action' },
]
const load = async (dimensionId: string) => {
rules.value = await getRuleList(dimensionId)
}
onMounted(() => {
//
load('default-dimension-id')
})
const handleDelete = async (id: string) => {
await deleteRule(id)
Message.success('删除成功')
load('default-dimension-id') //
}
</script>