新增员工统计板块

This commit is contained in:
fuenmao
2026-09-11 10:39:17 +08:00
parent 4371eca6c4
commit 446e83eed1
4 changed files with 324 additions and 18 deletions
+19 -3
View File
@@ -313,6 +313,21 @@ export interface OnboardingEmployee {
createTime?: string; createTime?: string;
} }
/** 不受快捷维度筛选影响的正式员工统计。 */
export interface OnboardingEmployeeStatistics {
total: number;
fullTime: number;
partTime: number;
probation: number;
socialPurchased: number;
expiringContract: number;
}
/** 附带统计的正式员工档案分页结果。 */
export interface OnboardingEmployeePage extends ResPage<OnboardingEmployee> {
statistics: OnboardingEmployeeStatistics;
}
export interface OnboardingFormField { export interface OnboardingFormField {
id: number; id: number;
createUserId?: number | string; createUserId?: number | string;
@@ -824,9 +839,10 @@ export const onboardingTaskApi = {
}, },
employeePage(params: Record<string, unknown>) { employeePage(params: Record<string, unknown>) {
return moduleRequest("/biz/onboarding/employee/").get< return moduleRequest("/biz/onboarding/employee/").get<OnboardingEmployeePage>(
ResPage<OnboardingEmployee> "page",
>("page", params); params
);
}, },
employeeExport(params: Record<string, unknown>) { employeeExport(params: Record<string, unknown>) {
+273 -6
View File
@@ -1,10 +1,32 @@
<template> <template>
<div class="table-box"> <div class="table-box">
<ProTable ref="proTable" title="员工档案" :columns="columns" :request-api="onboardingTaskApi.employeePage"> <ProTable ref="proTable" title="员工档案" :columns="columns" :request-api="getTableData">
<template #tableHeader> <template #tableHeader>
<el-button v-auth="'onboardingEmployeeExport'" type="primary" plain icon="Download" :loading="exporting" @click="exportEmployees"> <div class="employee-table-toolbar">
导出当前筛选结果 <el-button v-auth="'onboardingEmployeeExport'" type="primary" plain icon="Download" :loading="exporting" @click="exportEmployees">
</el-button> 导出当前筛选结果
</el-button>
<div class="employee-metric-filters">
<el-tooltip
v-for="item in metricFilters"
:key="item.key"
:content="item.tooltip"
placement="top"
>
<button
type="button"
class="metric-filter"
:class="[`tone-${item.tone}`, { 'is-active': activeMetricKey === item.key }]"
:aria-pressed="activeMetricKey === item.key"
@click="toggleMetricFilter(item.key)"
>
<el-icon><component :is="item.icon" /></el-icon>
<span class="metric-filter-label">{{ item.label }}</span>
<strong class="metric-filter-count">{{ item.count }}</strong>
</button>
</el-tooltip>
</div>
</div>
</template> </template>
<template #operation="scope"> <template #operation="scope">
@@ -523,7 +545,17 @@
<script setup lang="ts" name="onboardingEmployee"> <script setup lang="ts" name="onboardingEmployee">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue"; import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
import { Document, Download, UserFilled } from "@element-plus/icons-vue"; import {
Briefcase,
Clock,
Connection,
Document,
Download,
FirstAidKit,
User,
UserFilled,
WarningFilled
} from "@element-plus/icons-vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import type { UploadFile, UploadRequestOptions, UploadUserFile } from "element-plus"; import type { UploadFile, UploadRequestOptions, UploadUserFile } from "element-plus";
import { import {
@@ -540,6 +572,135 @@ import { useOnboardingPermission } from "@/utils/onboardingPermission";
const { canManage, disabledReason } = useOnboardingPermission("onboardingEmployeeManageAll"); const { canManage, disabledReason } = useOnboardingPermission("onboardingEmployeeManageAll");
const proTable = ref<ProTableInstance>(); const proTable = ref<ProTableInstance>();
// 统计数量
const statistics = reactive({
total: 0,
fullTime: 0,
partTime: 0,
probation: 0,
socialPurchased: 0,
expiringContract: 0
});
const activeMetricKey = ref<string>("total");
const metricFilters = computed(() => [
{
key: "total",
label: "在册总数",
count: statistics.total,
icon: User,
tone: "blue",
tooltip: activeMetricKey.value === "total" ? "当前显示全部在册员工" : "点击查看全部在册员工"
},
{
key: "full_time",
label: "正式员工",
count: statistics.fullTime,
icon: Briefcase,
tone: "green",
tooltip: activeMetricKey.value === "full_time" ? "再次点击取消筛选" : "点击筛选正式员工"
},
{
key: "part_time",
label: "兼职员工",
count: statistics.partTime,
icon: Connection,
tone: "purple",
tooltip: activeMetricKey.value === "part_time" ? "再次点击取消筛选" : "点击筛选兼职员工"
},
{
key: "probation",
label: "试用期待转正",
count: statistics.probation,
icon: Clock,
tone: "orange",
tooltip: activeMetricKey.value === "probation" ? "再次点击取消筛选" : "点击筛选试用期待转正员工"
},
{
key: "social_purchased",
label: "社保已缴纳",
count: statistics.socialPurchased,
icon: FirstAidKit,
tone: "teal",
tooltip: activeMetricKey.value === "social_purchased" ? "再次点击取消筛选" : "点击筛选社保已缴纳员工"
},
{
key: "expiring_contract",
label: "合同即将到期",
count: statistics.expiringContract,
icon: WarningFilled,
tone: "red",
tooltip: activeMetricKey.value === "expiring_contract" ? "再次点击取消筛选" : "点击筛选合同30天内到期或已到期员工"
}
]);
/** 点击统计项筛选员工,再次点击同一项取消筛选并返回全部。 */
function toggleMetricFilter(key: string) {
if (!proTable.value?.searchParam) return;
const isAlreadyActive = activeMetricKey.value === key;
// 清空快捷维度相关筛选
proTable.value.searchParam.employmentType = undefined;
proTable.value.searchParam.regularizationStatus = undefined;
proTable.value.searchParam.socialInsuranceStatus = undefined;
proTable.value.searchParam.contractExpiringSoon = undefined;
if (isAlreadyActive || key === "total") {
activeMetricKey.value = "total";
} else {
activeMetricKey.value = key;
if (key === "full_time") {
proTable.value.searchParam.employmentType = "full_time";
} else if (key === "part_time") {
proTable.value.searchParam.employmentType = "part_time";
} else if (key === "probation") {
proTable.value.searchParam.regularizationStatus = "probation";
} else if (key === "social_purchased") {
proTable.value.searchParam.socialInsuranceStatus = "purchased";
} else if (key === "expiring_contract") {
proTable.value.searchParam.contractExpiringSoon = true;
}
}
proTable.value.search();
}
/** 列表查询及统计数据刷新 */
async function getTableData(params: any) {
const res = await onboardingTaskApi.employeePage(params);
// 同步高亮选中的统计徽章
if (params.contractExpiringSoon === true || params.contractExpiringSoon === "true") {
activeMetricKey.value = "expiring_contract";
} else if (params.regularizationStatus === "probation" && !params.employmentType && !params.socialInsuranceStatus) {
activeMetricKey.value = "probation";
} else if (params.socialInsuranceStatus === "purchased" && !params.employmentType && !params.regularizationStatus) {
activeMetricKey.value = "social_purchased";
} else if (params.employmentType === "full_time" && !params.regularizationStatus && !params.socialInsuranceStatus) {
activeMetricKey.value = "full_time";
} else if (params.employmentType === "part_time" && !params.regularizationStatus && !params.socialInsuranceStatus) {
activeMetricKey.value = "part_time";
} else if (!params.employmentType && !params.regularizationStatus && !params.socialInsuranceStatus && !params.contractExpiringSoon) {
activeMetricKey.value = "total";
} else {
activeMetricKey.value = "";
}
const stats = res.data?.statistics;
if (stats) {
statistics.total = stats.total ?? 0;
statistics.fullTime = stats.fullTime ?? 0;
statistics.partTime = stats.partTime ?? 0;
statistics.probation = stats.probation ?? 0;
statistics.socialPurchased = stats.socialPurchased ?? 0;
statistics.expiringContract = stats.expiringContract ?? 0;
}
return res;
}
const detailVisible = ref(false); const detailVisible = ref(false);
const activeTab = ref("basic"); const activeTab = ref("basic");
const detail = ref<any>(); const detail = ref<any>();
@@ -1677,4 +1838,110 @@ onBeforeUnmount(clearMaterialPreviews);
color: var(--el-color-primary); color: var(--el-color-primary);
font-weight: 500; font-weight: 500;
} }
</style>
/* 员工统计与快捷筛选工具栏 */
.employee-table-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
width: 100%;
}
.employee-metric-filters {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.metric-filter {
--metric-color: #64748b;
--metric-bg: #f8fafc;
display: inline-flex;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 10px;
color: var(--el-text-color-regular);
font: inherit;
font-size: 13px;
white-space: nowrap;
cursor: pointer;
background: #fff;
border: 1px solid var(--el-border-color);
border-radius: 6px;
transition: all 0.18s ease;
}
.metric-filter:hover {
color: var(--metric-color);
border-color: var(--metric-color);
transform: translateY(-1px);
}
.metric-filter.is-active {
color: var(--metric-color);
background: var(--metric-bg);
border-color: var(--metric-color);
box-shadow: inset 0 0 0 1px var(--metric-color);
}
.metric-filter.tone-blue {
--metric-color: #2563eb;
--metric-bg: #eff6ff;
}
.metric-filter.tone-green {
--metric-color: #059669;
--metric-bg: #ecfdf5;
}
.metric-filter.tone-purple {
--metric-color: #7c3aed;
--metric-bg: #f5f3ff;
}
.metric-filter.tone-orange {
--metric-color: #d97706;
--metric-bg: #fffbeb;
}
.metric-filter.tone-teal {
--metric-color: #0d9488;
--metric-bg: #f0fdfa;
}
.metric-filter.tone-red {
--metric-color: #dc2626;
--metric-bg: #fef2f2;
}
.metric-filter .el-icon {
color: var(--metric-color);
font-size: 15px;
}
.metric-filter-label {
line-height: 1;
}
.metric-filter-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
color: var(--metric-color);
font-size: 12px;
line-height: 20px;
background: var(--metric-bg);
border-radius: 10px;
}
.metric-filter.is-active .metric-filter-count {
color: #fff;
background: var(--metric-color);
}
</style>
@@ -73,7 +73,7 @@
<!-- 资料类型弹窗 --> <!-- 资料类型弹窗 -->
<el-dialog v-model="materialVisible" :title="materialForm.id ? '编辑资料类型' : '新增资料类型'" width="560px" destroy-on-close> <el-dialog v-model="materialVisible" :title="materialForm.id ? '编辑资料类型' : '新增资料类型'" width="560px" destroy-on-close>
<el-form :model="materialForm" label-width="110px"> <el-form :model="materialForm" label-width="120px" class="material-form">
<el-form-item label="资料编码" required> <el-form-item label="资料编码" required>
<el-input v-model="materialForm.materialCode" /> <el-input v-model="materialForm.materialCode" />
</el-form-item> </el-form-item>
@@ -88,10 +88,12 @@
<el-form-item label="最大文件数"> <el-form-item label="最大文件数">
<el-input-number v-model="materialForm.maxFileCount" :min="1" :max="10" /> <el-input-number v-model="materialForm.maxFileCount" :min="1" :max="10" />
<span class="unit-suffix">个</span>
</el-form-item> </el-form-item>
<el-form-item label="单文件大小(MB)"> <el-form-item label="单文件大小">
<el-input-number v-model="materialForm.maxFileSizeMb" :min="1" :max="100" /> <el-input-number v-model="materialForm.maxFileSizeMb" :min="1" :max="100" />
<span class="unit-suffix">MB</span>
</el-form-item> </el-form-item>
<el-form-item label="状态"> <el-form-item label="状态">
@@ -244,4 +246,14 @@ onMounted(() => {
.toolbar { .toolbar {
margin-bottom: 12px; margin-bottom: 12px;
} }
.material-form :deep(.el-form-item__label) {
white-space: nowrap;
}
.unit-suffix {
margin-left: 10px;
color: var(--el-text-color-secondary);
font-size: 13px;
}
</style> </style>
+18 -7
View File
@@ -199,6 +199,7 @@
:model="companyForm" :model="companyForm"
:rules="companyRules" :rules="companyRules"
label-width="140px" label-width="140px"
class="setup-form"
> >
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
@@ -225,6 +226,7 @@
placeholder="18位统一社会信用代码" placeholder="18位统一社会信用代码"
maxlength="18" maxlength="18"
clearable clearable
@input="val => companyForm.unifiedSocialCreditCode = (val || '').toUpperCase()"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -497,7 +499,7 @@ const companyRules = reactive<FormRules>({
{ required: true, message: "请输入公司编码", trigger: "blur" }, { required: true, message: "请输入公司编码", trigger: "blur" },
{ {
pattern: /^[a-zA-Z0-9_-]{2,50}$/, pattern: /^[a-zA-Z0-9_-]{2,50}$/,
message: "公司编码格式不正确,支持2-50位字母、数字、下划线或短横线", message: "支持2-50位字母、数字、下划线或短横线",
trigger: "blur" trigger: "blur"
} }
], ],
@@ -512,8 +514,8 @@ const companyRules = reactive<FormRules>({
unifiedSocialCreditCode: [ unifiedSocialCreditCode: [
{ required: true, message: "请输入统一社会信用代码", trigger: "blur" }, { required: true, message: "请输入统一社会信用代码", trigger: "blur" },
{ {
pattern: /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/i, pattern: /^[0-9A-Za-z]{18}$/,
message: "统一社会信用代码格式不正确,请输入18位标准社会信用代码", message: "请输入18位统一社会信用代码",
trigger: "blur" trigger: "blur"
} }
], ],
@@ -521,7 +523,7 @@ const companyRules = reactive<FormRules>({
{ required: true, message: "请输入法定代表人姓名", trigger: "blur" }, { required: true, message: "请输入法定代表人姓名", trigger: "blur" },
{ {
pattern: /^[\u4e00-\u9fa5a-zA-Z·.\s]{2,20}$/, pattern: /^[\u4e00-\u9fa5a-zA-Z·.\s]{2,20}$/,
message: "法定代表人姓名格式不正确,请输入2-20位中英文姓名", message: "请输入2-20位中英文姓名",
trigger: "blur" trigger: "blur"
} }
], ],
@@ -529,7 +531,7 @@ const companyRules = reactive<FormRules>({
{ required: true, message: "请输入企业联系电话", trigger: "blur" }, { required: true, message: "请输入企业联系电话", trigger: "blur" },
{ {
pattern: /^(1[3-9]\d{9}|(0\d{2,3}-?)?\d{7,8}|400(-?\d{3,4}){2})$/, pattern: /^(1[3-9]\d{9}|(0\d{2,3}-?)?\d{7,8}|400(-?\d{3,4}){2})$/,
message: "联系电话格式不正确,请输入有效的手机号或固话号码", message: "请输入有效的手机号或固话号码",
trigger: "blur" trigger: "blur"
} }
], ],
@@ -543,14 +545,14 @@ const companyRules = reactive<FormRules>({
qiyuesuoCategoryId: [ qiyuesuoCategoryId: [
{ {
pattern: /^\d*$/, pattern: /^\d*$/,
message: "契约锁业务分类 Id 必须为纯数字", message: "业务分类 Id 必须为纯数字",
trigger: "blur" trigger: "blur"
} }
], ],
qiyuesuoSealId: [ qiyuesuoSealId: [
{ {
pattern: /^\d*$/, pattern: /^\d*$/,
message: "契约锁企业印章 Id 必须为纯数字", message: "企业印章 Id 必须为纯数字",
trigger: "blur" trigger: "blur"
} }
] ]
@@ -960,4 +962,13 @@ onMounted(loadCompanies);
.w-full { .w-full {
width: 100%; width: 100%;
} }
.setup-form :deep(.el-form-item) {
margin-bottom: 22px;
}
.setup-form :deep(.el-form-item__error) {
padding-top: 3px;
line-height: 1.25;
}
</style> </style>