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

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-17 00:54:51 +08:00

246 lines
7.4 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.

<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>