feat(frontend): 绩效台账与月度结算两页接入(新组绩效工资)

- 绩效台账页(order17):月/版本筛选 + 行展开计算快照 + 重算(原因
  对话框,管理员/财务);核算入口在回款管理页(每笔回款行内触发)
- 月结页(order18):生成快照(月份输入)+ 状态筛选 + 提交审批 +
  详情抽屉(按人明细三口径/试用期说明/审批单号关联)
- 均复用 usePagedList/AppPagination/--ys-* 惯例;金额 str 直出
- type-check 零错误;build 通过

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
liuwenxinandClaude Code committed 2026-10-17 00:54:51 +08:00
1 parent e7e47d88bc
commit a1217f4482
5 files changed
+609

No files matched your search

+245
View File
@@ -0,0 +1,245 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import {
generateSettlement,
getSettlement,
listSettlements,
settlementStatusLabel,
settlementStatusTagType,
submitSettlement,
type Settlement,
type SettlementDetail,
} from '@/api/settlements'
import AppPagination from '@/components/table/AppPagination.vue'
import { usePagedList } from '@/composables/usePagedList'
import { handledError } from '@/utils/error'
import { formatDateTime } from '@/utils/format'
/**
* 月度结算(M3 ④):按确认回款生成快照 → 提交审批 → 通过即锁定。
*
* 锁定后源数据改动被拦截(回款/发货写路径 400),调整走调整单。
* 详情抽屉展示按人明细(三口径净工资与试用期说明)。
*/
const filters = reactive({
status: undefined as number | undefined,
})
const { records, total, pageNum, pageSize, loading, load, search } = usePagedList<
Settlement,
{ status?: number }
>(listSettlements, () => ({ status: filters.status }))
function resetFilters(): void {
filters.status = undefined
search()
}
const generateMonth = ref('')
const generating = ref(false)
async function handleGenerate(): Promise<void> {
if (!/^\d{4}-\d{2}$/.test(generateMonth.value)) {
ElMessage.warning('请输入结算月份(YYYY-MM)')
return
}
generating.value = true
try {
await generateSettlement(generateMonth.value)
ElMessage.success('月结已生成(草稿)')
await search()
} catch (error) {
handledError(error)
} finally {
generating.value = false
}
}
const detailVisible = ref(false)
const detailLoading = ref(false)
const detail = ref<SettlementDetail | null>(null)
async function openDetail(row: Settlement): Promise<void> {
detailVisible.value = true
detailLoading.value = true
try {
detail.value = await getSettlement(row.id)
} catch (error) {
handledError(error)
detailVisible.value = false
} finally {
detailLoading.value = false
}
}
const submitting = ref(false)
async function handleSubmit(row: Settlement): Promise<void> {
try {
const out = await submitSettlement(row.id)
ElMessage.success(
out.status === 3 ? '审批通过,月结已锁定' : '已提交审批(待财务复核)',
)
detail.value = await getSettlement(row.id)
await load()
} catch (error) {
handledError(error)
} finally {
submitting.value = false
}
}
</script>
<template>
<div class="page">
<div class="toolbar">
<div class="gen-row">
<el-input
v-model="generateMonth"
placeholder="结算月份 YYYY-MM"
style="width: 160px"
maxlength="7"
/>
<el-button type="primary" :loading="generating" @click="handleGenerate">
生成月结快照
</el-button>
</div>
</div>
<el-form inline class="filters" @submit.prevent>
<el-form-item label="状态">
<el-select v-model="filters.status" placeholder="全部" clearable style="width: 150px">
<el-option label="草稿" :value="1" />
<el-option label="待审批" :value="2" />
<el-option label="已审定(锁定)" :value="3" />
<el-option label="已驳回" :value="4" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="records" stripe>
<el-table-column prop="settlement_month" label="结算月份" width="100" />
<el-table-column label="状态" width="130">
<template #default="{ row }">
<el-tag :type="settlementStatusTagType(row.status)" size="small" disable-transitions>
{{ settlementStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="total_performance" label="绩效合计" min-width="110" />
<el-table-column prop="total_payable" label="应付合计" min-width="110" />
<el-table-column prop="person_count" label="人数" width="70" />
<el-table-column prop="approval_order_no" label="审批单号" min-width="150" />
<el-table-column label="提交时间" min-width="150">
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
</el-table-column>
<el-table-column label="操作" width="130" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">详情</el-button>
<el-button
v-if="row.status === 1 || row.status === 4"
link
type="success"
:loading="submitting"
@click="handleSubmit(row)"
>
提交审批
</el-button>
</template>
</el-table-column>
</el-table>
<AppPagination
v-model:page-num="pageNum"
v-model:page-size="pageSize"
:total="total"
@change="load"
/>
<el-drawer v-model="detailVisible" title="月结详情" size="560px">
<div v-loading="detailLoading">
<template v-if="detail">
<div class="detail-head">
<span class="detail-month">{{ detail.settlement_month }}</span>
<el-tag :type="settlementStatusTagType(detail.status)" size="small" disable-transitions>
{{ settlementStatusLabel(detail.status) }}
</el-tag>
</div>
<div class="detail-meta">
<span>审批单号:{{ detail.approval_order_no || '—' }}</span>
<span>提交人:{{ detail.submitter_name }}</span>
<span v-if="detail.locked_by">锁定人:{{ detail.locked_by }}</span>
</div>
<el-table :data="detail.items" size="small" stripe>
<el-table-column prop="personnel_name" label="人员" min-width="90" />
<el-table-column label="类型" width="90">
<template #default="{ row }">
{{ row.calc_type === 1 ? '单笔回款' : '经理聚合' }}
</template>
</el-table-column>
<el-table-column prop="performance_sum" label="绩效" min-width="90" />
<el-table-column prop="payable_sum" label="应付" min-width="90" />
<el-table-column prop="net_amount" label="净工资" min-width="90" />
<el-table-column label="试用期" width="70">
<template #default="{ row }">{{ row.is_probation === 1 ? '是' : '否' }}</template>
</el-table-column>
</el-table>
<p v-if="detail.status === 1 || detail.status === 4" class="hint">
草稿/已驳回状态可重新生成快照后再次提交。
</p>
<p v-else-if="detail.status === 3" class="hint">已锁定:源数据改动只产生提示与调整单。</p>
</template>
</div>
</el-drawer>
</div>
</template>
<style scoped>
.toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
.gen-row {
display: flex;
gap: 10px;
}
.filters {
margin-bottom: 4px;
}
.detail-head {
display: flex;
align-items: center;
gap: 10px;
}
.detail-month {
font-size: 15px;
font-weight: 600;
color: var(--ys-ink);
}
.detail-meta {
display: flex;
flex-direction: column;
gap: 2px;
padding: 10px 0;
font-size: 12px;
color: var(--ys-ink-3);
}
.hint {
margin-top: 12px;
font-size: 12px;
color: var(--ys-ink-3);
}
</style>