237 lines
8.2 KiB
Vue
237 lines
8.2 KiB
Vue
<script setup lang="ts">
|
|
import { Banknote, Building2, Clock3, Loader2, Search, WalletCards } from "@lucide/vue";
|
|
import { computed, onMounted, ref } from "vue";
|
|
|
|
import { ApiError } from "../api/http";
|
|
import { getPayrollReport } from "../api/reports";
|
|
import type { PayrollReportResponse } from "../api/types";
|
|
import DatePicker from "../components/DatePicker.vue";
|
|
|
|
const salaryMonth = ref(new Date().toISOString().slice(0, 7));
|
|
const report = ref<PayrollReportResponse | null>(null);
|
|
const loading = ref(false);
|
|
const errorMessage = ref("");
|
|
|
|
const totals = computed(() => report.value?.totals || {});
|
|
const attendance = computed(() => report.value?.attendance || {});
|
|
|
|
onMounted(loadReport);
|
|
|
|
async function loadReport(): Promise<void> {
|
|
loading.value = true;
|
|
errorMessage.value = "";
|
|
try {
|
|
report.value = await getPayrollReport(salaryMonth.value);
|
|
} catch (error) {
|
|
errorMessage.value = error instanceof ApiError ? error.message : "统计报表加载失败";
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function money(value: number | undefined): string {
|
|
return new Intl.NumberFormat("zh-CN", {
|
|
style: "currency",
|
|
currency: "CNY",
|
|
maximumFractionDigits: 2,
|
|
}).format(Number(value || 0));
|
|
}
|
|
|
|
function modeLabel(value: string): string {
|
|
const labels: Record<string, string> = {
|
|
monthly: "包月",
|
|
hourly: "计时",
|
|
piecework: "计件",
|
|
probation: "试用期",
|
|
};
|
|
return labels[value] || value;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="view-stack report-page">
|
|
<header class="page-header">
|
|
<div>
|
|
<h1>统计报表</h1>
|
|
<p>查看工资汇总、部门成本和考勤工时统计。</p>
|
|
</div>
|
|
<form class="header-actions" @submit.prevent="loadReport">
|
|
<label class="field inline-field">
|
|
<span>工资月份</span>
|
|
<DatePicker v-model="salaryMonth" mode="month" placeholder="请选择工资月份" />
|
|
</label>
|
|
<button class="primary-button" type="submit" :disabled="loading">
|
|
<Loader2 v-if="loading" class="spin" :size="18" aria-hidden="true" />
|
|
<Search v-else :size="18" aria-hidden="true" />
|
|
<span>{{ loading ? "查询中" : "查询" }}</span>
|
|
</button>
|
|
</form>
|
|
</header>
|
|
|
|
<p v-if="errorMessage" class="form-error">{{ errorMessage }}</p>
|
|
|
|
<section class="panel table-panel report-ledger-workspace report-record-panel" aria-label="工资明细台账">
|
|
<div class="report-ledger-toolbar">
|
|
<div class="report-ledger-title">
|
|
<span class="status-chip neutral">{{ report?.salary_month || salaryMonth }}</span>
|
|
<div>
|
|
<span class="report-ledger-eyebrow">工资汇总</span>
|
|
<h2>工资明细台账</h2>
|
|
</div>
|
|
</div>
|
|
<div class="report-ledger-totals" aria-label="工资汇总">
|
|
<div class="report-total-item">
|
|
<span>应发工资</span>
|
|
<strong>{{ money(totals.gross_salary) }}</strong>
|
|
<small>基础 + 提成 + 加班</small>
|
|
</div>
|
|
<div class="report-total-item">
|
|
<span>扣款合计</span>
|
|
<strong>{{ money(totals.deduction_amount) }}</strong>
|
|
<small>迟到 + 缺卡</small>
|
|
</div>
|
|
<div class="report-total-item is-primary">
|
|
<span>实发工资</span>
|
|
<strong>{{ money(totals.net_salary) }}</strong>
|
|
<small>财务最终金额</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="report-inline-metrics" aria-label="报表核心指标">
|
|
<span>
|
|
<WalletCards :size="15" aria-hidden="true" />
|
|
员工数
|
|
<strong>{{ totals.employee_count || 0 }}</strong>
|
|
</span>
|
|
<span>
|
|
<Banknote :size="15" aria-hidden="true" />
|
|
提成
|
|
<strong>{{ money(totals.commission_amount) }}</strong>
|
|
</span>
|
|
<span>
|
|
<Clock3 :size="15" aria-hidden="true" />
|
|
加班费
|
|
<strong>{{ money(totals.overtime_pay) }}</strong>
|
|
</span>
|
|
<span>
|
|
<Clock3 :size="15" aria-hidden="true" />
|
|
加班工时
|
|
<strong>{{ Number(attendance.overtime_hours || 0).toFixed(1) }}h</strong>
|
|
</span>
|
|
<span>
|
|
<Clock3 :size="15" aria-hidden="true" />
|
|
请假工时
|
|
<strong>{{ Number(attendance.leave_hours || 0).toFixed(1) }}h</strong>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="report-table-heading">
|
|
<div>
|
|
<h3>最近工资明细</h3>
|
|
<p>{{ report?.recent_records.length || 0 }} 条记录,按员工展示工资构成。</p>
|
|
</div>
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table class="data-table report-record-table">
|
|
<thead>
|
|
<tr>
|
|
<th>员工</th>
|
|
<th>部门 / 月份</th>
|
|
<th>模式</th>
|
|
<th>基础工资</th>
|
|
<th>提成</th>
|
|
<th>加班费</th>
|
|
<th>扣款</th>
|
|
<th>应发</th>
|
|
<th>实发</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="item in report?.recent_records || []" :key="item.id">
|
|
<td>
|
|
<strong>{{ item.employee_name }}</strong>
|
|
<span>{{ item.employee_no || "-" }}</span>
|
|
</td>
|
|
<td>
|
|
<strong>{{ item.department || "-" }}</strong>
|
|
<span>{{ item.salary_month }}</span>
|
|
</td>
|
|
<td><span class="status-chip neutral">{{ modeLabel(item.salary_mode) }}</span></td>
|
|
<td class="mono">{{ money(item.base_pay) }}</td>
|
|
<td class="mono">{{ money(item.commission_amount) }}</td>
|
|
<td class="mono">{{ money(item.overtime_pay) }}</td>
|
|
<td class="mono">{{ money(item.deduction_amount) }}</td>
|
|
<td class="mono">{{ money(item.gross_salary) }}</td>
|
|
<td class="mono strong report-net-cell">{{ money(item.net_salary) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="!report?.recent_records.length && !loading" class="empty-state">
|
|
<p>暂无工资记录</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="report-detail-grid">
|
|
<div class="panel table-panel">
|
|
<div class="panel-header">
|
|
<div>
|
|
<h2>部门工资汇总</h2>
|
|
<p>按部门查看人数、应发和实发成本。</p>
|
|
</div>
|
|
<Building2 :size="22" aria-hidden="true" />
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>部门</th>
|
|
<th>人数</th>
|
|
<th>应发</th>
|
|
<th>实发</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="item in report?.departments || []" :key="item.department">
|
|
<td><strong>{{ item.department }}</strong></td>
|
|
<td>{{ item.employee_count }}</td>
|
|
<td class="mono">{{ money(item.gross_salary) }}</td>
|
|
<td class="mono strong">{{ money(item.net_salary) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div v-if="!report?.departments.length && !loading" class="empty-state">
|
|
<p>暂无部门汇总</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel report-attendance-panel">
|
|
<div class="panel-header">
|
|
<div>
|
|
<h2>考勤工时拆分</h2>
|
|
<p>加班和请假会影响剩余加班及工资结果。</p>
|
|
</div>
|
|
<Clock3 :size="22" aria-hidden="true" />
|
|
</div>
|
|
<div class="report-attendance-list">
|
|
<div class="report-attendance-card">
|
|
<span>加班工时</span>
|
|
<strong>{{ Number(attendance.overtime_hours || 0).toFixed(1) }}h</strong>
|
|
</div>
|
|
<div class="report-attendance-card">
|
|
<span>请假工时</span>
|
|
<strong>{{ Number(attendance.leave_hours || 0).toFixed(1) }}h</strong>
|
|
</div>
|
|
<div class="report-attendance-card">
|
|
<span>缺勤天数</span>
|
|
<strong>{{ Number(attendance.absence_days || 0).toFixed(1) }}</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
</template>
|