菜单页面文件创建、路由配置、入职财务登记api实现、开票申请api实现

This commit is contained in:
雷校云
2025-12-10 11:26:37 +08:00
parent 29d7fb412c
commit b1be29c9a1
35 changed files with 12044 additions and 76 deletions

View File

@@ -1,6 +1,10 @@
import request from "@/utils/request";
const AUTH_BASE_URL = "/api2";
/*
* 部门管理
* */
// 公司部门列表
export const UserDepartment = (name: string) => {
const formData = new FormData();
@@ -43,7 +47,7 @@ export const UserDeleteDepartment = (id: string) => {
});
};
// 部门分页
// 部门分页查询
export const UserPersonlist = (data: any) => {
const formData = new FormData();
formData.append("per_page", data.pageSize);

View File

@@ -0,0 +1,43 @@
import request from "@/utils/request";
const AUTH_BASE_URL = "/api2";
/*
* 开票申请
* */
// 开发票申请
export const FinanceIssueInvoice = (data: any) => {
const formData = new FormData();
formData.append("ContractNo", data.ContractNo);
formData.append("personincharge", data.personincharge);
formData.append("amount", data.amount);
formData.append("type", data.type);
formData.append("unit", data.unit);
formData.append("number", data.number);
formData.append("address_telephone", data.address_telephone);
formData.append("bank", data.bank);
formData.append("username", data.username);
return request({
url: `${AUTH_BASE_URL}/finance/issue-invoice`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 开票分页查询
export const FinanceIssueDetail = (data: any) => {
const formData = new FormData();
formData.append("page", data.page);
formData.append("per_page", data.per_page);
return request({
url: `${AUTH_BASE_URL}/finance/issue-Detail`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};

View File

@@ -1,5 +1,9 @@
import request from "@/utils/request";
/*
* 登录
* */
// const AUTH_BASE_URL = "http://8.137.99.82:8006";
const AUTH_BASE_URL = "/api2";
export const userLogin = (data: any) => {

View File

@@ -0,0 +1,25 @@
import request from "@/utils/request";
const AUTH_BASE_URL = "/api2";
/*
* 入职财务登记
* */
// 入职财务
export const FinanceUserRegister = (data: any) => {
const formData = new FormData();
formData.append("username", data.username);
formData.append("card", data.card);
formData.append("Dateofjoining", data.Dateofjoining);
formData.append("position", data.position);
formData.append("salary", data.salary);
formData.append("personincharge", data.personincharge);
return request({
url: `${AUTH_BASE_URL}/finance/user-register`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};

View File

@@ -2,7 +2,11 @@ import { isFile, isString } from "@/utils/auxiliaryFunction";
import request from "@/utils/request";
const AUTH_BASE_URL = "/api2";
// 人员列表
/*
* 人事管理
* */
// 人员分页查询
export const UserPersonnelList = (data: any) => {
const formData = new FormData();
formData.append("page", data.pageNum);

View File

@@ -72,7 +72,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "user",
name: "PersonnelUser",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/personnelManagement/index.vue"),
meta: {
title: "人事管理",
},
@@ -80,7 +80,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "role",
name: "PersonnelRole",
component: () => import("@/views/system/role/index.vue"),
component: () => import("@/views/calibration/department/index.vue"),
meta: {
title: "角色管理",
},
@@ -98,9 +98,10 @@ export const constantRoutes: RouteRecordRaw[] = [
},
children: [
{
path: "joining",
name: "Joining",
component: () => import("@/views/system/user/index.vue"),
path: "onboardingRegistration",
name: "OnboardingRegistration",
component: () => import("@/views/calibration/onboardingRegistration/index.vue"),
// component: () => import("@/views/system/user/index.vue"),
meta: {
title: "入职财务登记",
},
@@ -108,7 +109,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "departure ",
name: "Departure",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/departureFinancialRegistration/index.vue"),
meta: {
title: "离职财务登记",
},
@@ -116,7 +117,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "invoiceApplication",
name: "InvoiceApplication",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/invoiceApplication/index.vue"),
meta: {
title: "开票申请",
},
@@ -124,7 +125,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "revenueRecognition",
name: "RevenueRecognition",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/revenueRecognition/index.vue"),
meta: {
title: "收入确认",
},
@@ -132,7 +133,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "accountAdjustmentApplication",
name: "AccountAdjustmentApplication",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/accountAdjustmentApplication/index.vue"),
meta: {
title: "调账申请",
},
@@ -140,7 +141,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "paymentApplicationForm",
name: "PaymentApplicationForm",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/paymentApplicationForm/index.vue"),
meta: {
title: "付款申请单",
},
@@ -148,7 +149,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "reimbursement",
name: "Reimbursement",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/reimbursement/index.vue"),
meta: {
title: "报销",
},
@@ -156,7 +157,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: "salaryBonusAdjustment",
name: "SalaryBonusAdjustment",
component: () => import("@/views/system/user/index.vue"),
component: () => import("@/views/calibration/salaryBonusAdjustment/index.vue"),
meta: {
title: "工资/奖金变更",
},

View File

@@ -33,68 +33,6 @@ export function throttle<T extends (...args: any[]) => any>(
};
}
// 处理后端返回的JSON字符串 正确格式化
export function formatJsonString(jsonString: string) {
// 移除首尾空白字符
jsonString = jsonString.trim();
// 首先检查是否为空字符串
if (!jsonString) return '""';
try {
// 尝试直接解析处理标准JSON
return JSON.stringify(JSON.parse(jsonString), null, 2);
} catch (error) {
// 增强版处理策略,特别优化对单引号数组的处理
try {
// 针对数组类型的特殊处理
if (jsonString.startsWith("[") && jsonString.endsWith("]")) {
// 处理数组中的单引号元素
const arrayFixed = jsonString
// 替换数组中所有单引号包裹的元素为双引号
.replace(/'([^']*)'/g, '"$1"');
try {
return JSON.stringify(JSON.parse(arrayFixed), null, 2);
} catch (arrayError) {
// 如果仍失败,尝试更细致的处理
const detailedFixed = jsonString
// 1. 先处理特殊情况:确保数组元素之间的逗号正确处理
.replace(/',\s*'/g, '", "')
// 2. 处理数组首尾的单引号
.replace(/^\['/, '["')
.replace(/'\]$/, '"]');
return JSON.stringify(JSON.parse(detailedFixed), null, 2);
}
}
// 通用处理策略
const fixedJson = jsonString
// 1. 处理对象键名的单引号
.replace(/([{\[,\s])\s*'([^']+?)'\s*:/g, '$1"$2":')
// 2. 处理数组元素的单引号
.replace(/(\[|,)\s*'([^']*?)'(?=\s*[\],])/g, '$1"$2"')
// 3. 处理对象值的单引号
.replace(/:\s*'([^']*?)'\s*([}\],]|$)/g, ': "$1"$2');
return JSON.stringify(JSON.parse(fixedJson), null, 2);
} catch (secondError) {
// 最后的尝试:使用更简单直接的方法
try {
// 最基础的处理:将所有单引号替换为双引号
// 这对于简单的单引号格式JSON很有效
const basicFix = jsonString.replace(/'/g, '"');
return JSON.stringify(JSON.parse(basicFix), null, 2);
} catch (finalError) {
// 如果所有尝试都失败,返回原始字符串
console.warn("无法解析的JSON字符串:", jsonString);
return jsonString;
}
}
}
}
/**
* 将文件路径数组转换为对象数组
* @param filePaths 文件路径数组或单个文件路径字符串

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>

View File

@@ -0,0 +1,444 @@
<template>
<div class="app-container">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item prop="keywords" label="关键字">
<el-input
v-model="queryParams.keywords"
placeholder="角色名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog()">新增</el-button>
<el-button
type="danger"
:disabled="ids.length === 0"
icon="delete"
@click="handleDelete()"
>
删除
</el-button>
</div>
</div>
<el-table
ref="dataTableRef"
v-loading="loading"
:data="roleList"
highlight-current-row
border
class="data-table__content"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="部门名称" prop="department" />
<el-table-column label="在职情况" prop="state" />
<el-table-column fixed="right" label="操作" width="220">
<template #default="scope">
<el-button
type="primary"
size="small"
link
icon="edit"
@click="handleOpenDialog(scope.row.id)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
link
icon="delete"
@click="onUserDeleteDepartment(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchData"
/>
</el-card>
<!-- 角色表单弹窗 -->
<el-dialog
v-model="dialog.visible"
:title="dialog.title"
width="500px"
@close="handleCloseDialog"
>
<el-form ref="roleFormRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="部门名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入部门名称" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="handleCloseDialog"> </el-button>
</div>
</template>
</el-dialog>
<!-- 分配权限弹窗 -->
<el-drawer
v-model="assignPermDialogVisible"
:title="'【' + checkedRole.name + '】权限分配'"
:size="drawerSize"
>
<div class="flex-x-between">
<el-input v-model="permKeywords" clearable class="w-[150px]" placeholder="菜单权限名称">
<template #prefix>
<Search />
</template>
</el-input>
<div class="flex-center ml-5">
<el-button type="primary" size="small" plain @click="togglePermTree">
<template #icon>
<Switch />
</template>
{{ isExpanded ? "收缩" : "展开" }}
</el-button>
<el-checkbox
v-model="parentChildLinked"
class="ml-5"
@change="handleparentChildLinkedChange"
>
父子联动
</el-checkbox>
<el-tooltip placement="bottom">
<template #content>
如果只需勾选菜单权限不需要勾选子菜单或者按钮权限请关闭父子联动
</template>
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<el-tree
ref="permTreeRef"
node-key="value"
show-checkbox
:data="menuPermOptions"
:filter-node-method="handlePermFilter"
:default-expand-all="true"
:check-strictly="!parentChildLinked"
class="mt-5"
>
<template #default="{ data }">
{{ data.label }}
</template>
</el-tree>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleAssignPermSubmit"> </el-button>
<el-button @click="assignPermDialogVisible = false"> </el-button>
</div>
</template>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { useAppStore } from "@/store/modules/app-store";
import { DeviceEnum } from "@/enums/settings/device-enum";
import RoleAPI, { RolePageVO } from "@/api/system/role-api";
import {
UserAddDepartment,
UserDeleteDepartment,
UserPersonlist,
} from "@/api/calibration/department";
defineOptions({
name: "Role",
inheritAttrs: false,
});
const appStore = useAppStore();
const queryFormRef = ref();
const roleFormRef = ref();
const permTreeRef = ref();
const loading = ref(false);
const ids = ref<number[]>([]);
const total = ref(0);
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
});
// 角色表格数据
const roleList = ref<RolePageVO[]>();
// 菜单权限下拉
const menuPermOptions = ref<OptionType[]>([]);
// 弹窗
const dialog = reactive({
title: "",
visible: false,
});
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// 角色表单
const formData = reactive<any>({
name: "",
});
const rules = reactive({
name: [{ required: true, message: "请输入部门名称", trigger: "blur" }],
});
// 选中的角色
interface CheckedRole {
id?: string;
name?: string;
}
const checkedRole = ref<CheckedRole>({});
const assignPermDialogVisible = ref(false);
const permKeywords = ref("");
const isExpanded = ref(true);
const parentChildLinked = ref(true);
// 获取数据
function fetchData() {
loading.value = true;
UserPersonlist(queryParams)
.then((res: any) => {
roleList.value = res.data;
total.value = res.total;
})
.finally(() => {
loading.value = false;
});
}
// 查询(重置页码后获取数据)
function handleQuery() {
queryParams.pageNum = 1;
fetchData();
}
// 重置查询
function handleResetQuery() {
queryFormRef.value.resetFields();
queryParams.pageNum = 1;
fetchData();
}
// 行复选框选中
function handleSelectionChange(selection: any) {
ids.value = selection.map((item: any) => item.id);
}
// 打开角色弹窗
function handleOpenDialog(roleId?: string) {
dialog.visible = true;
if (roleId) {
dialog.title = "修改角色";
RoleAPI.getFormData(roleId).then((data) => {
Object.assign(formData, data);
});
} else {
dialog.title = "新增角色";
}
}
// 提交角色表单
function handleSubmit() {
roleFormRef.value.validate((valid: any) => {
if (valid) {
loading.value = true;
const roleId = formData.id;
if (roleId) {
RoleAPI.update(roleId, formData)
.then(() => {
ElMessage.success("修改成功");
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
} else {
UserAddDepartment(formData.name)
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
handleResetQuery();
})
.finally(() => (loading.value = false));
}
}
});
}
// 关闭弹窗
function handleCloseDialog() {
dialog.visible = false;
roleFormRef.value.resetFields();
roleFormRef.value.clearValidate();
formData.id = undefined;
formData.sort = 1;
formData.status = 1;
}
const onUserDeleteDepartment = (id: string) => {
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => {
loading.value = true;
UserDeleteDepartment(id)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
() => {
ElMessage.info("已取消删除");
}
);
};
// 删除角色
function handleDelete(roleId?: number) {
const roleIds = [roleId || ids.value].join(",");
if (!roleIds) {
ElMessage.warning("请勾选删除项");
return;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => {
loading.value = true;
RoleAPI.deleteByIds(roleIds)
.then(() => {
ElMessage.success("删除成功");
handleResetQuery();
})
.finally(() => (loading.value = false));
},
() => {
ElMessage.info("已取消删除");
}
);
}
// 打开分配菜单权限弹窗
// async function handleOpenAssignPermDialog(row: RolePageVO) {
// const roleId = row.id;
// if (roleId) {
// assignPermDialogVisible.value = true;
// loading.value = true;
//
// checkedRole.value.id = roleId;
// checkedRole.value.name = row.name;
//
// // 获取所有的菜单
// menuPermOptions.value = await MenuAPI.getOptions();
//
// // 回显角色已拥有的菜单
// RoleAPI.getRoleMenuIds(roleId)
// .then((data) => {
// const checkedMenuIds = data;
// checkedMenuIds.forEach((menuId) => permTreeRef.value!.setChecked(menuId, true, false));
// })
// .finally(() => {
// loading.value = false;
// });
// }
// }
// 分配菜单权限提交
function handleAssignPermSubmit() {
const roleId = checkedRole.value.id;
if (roleId) {
const checkedMenuIds: number[] = permTreeRef
.value!.getCheckedNodes(false, true)
.map((node: any) => node.value);
loading.value = true;
RoleAPI.updateRoleMenus(roleId, checkedMenuIds)
.then(() => {
ElMessage.success("分配权限成功");
assignPermDialogVisible.value = false;
handleResetQuery();
})
.finally(() => {
loading.value = false;
});
}
}
// 展开/收缩 菜单权限树
function togglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
Object.values(permTreeRef.value.store.nodesMap).forEach((node: any) => {
if (isExpanded.value) {
node.expand();
} else {
node.collapse();
}
});
}
}
// 权限筛选
watch(permKeywords, (val) => {
permTreeRef.value!.filter(val);
});
function handlePermFilter(
value: string,
data: {
[key: string]: any;
}
) {
if (!value) return true;
return data.label.includes(value);
}
// 父子菜单节点是否联动
function handleparentChildLinkedChange(val: any) {
parentChildLinked.value = val;
}
onMounted(() => {
handleQuery();
});
</script>

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>

View File

@@ -0,0 +1,83 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
<el-tree
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'name', disabled: '' }"
node-key="id"
highlight-current
:current-node-key="deptList?.length ? deptList[0].id : ''"
:expand-on-click-node="false"
:filter-node-method="handleFilter"
default-expand-all
@node-click="handleNodeClick"
/>
</el-card>
</template>
<script setup lang="ts">
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
default: undefined,
},
});
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
function handleFilter(value: string, data: any) {
if (!value) {
return true;
}
return data.label.indexOf(value) !== -1;
}
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.name;
emits("node-click");
}
const DepartmentList = (name: string = "") => {
UserDepartment(name).then((res: any) => {
deptList.value = res.data;
if (deptList.value && deptList.value.length > 0) {
deptId.value = deptList.value[0].name;
}
});
};
onBeforeMount(() => {
DepartmentList();
});
</script>

View File

@@ -0,0 +1,198 @@
<template>
<div>
<el-dialog
v-model="visible"
:align-center="true"
title="导入数据"
width="600px"
@close="handleClose"
>
<el-scrollbar max-height="60vh">
<el-form
ref="importFormRef"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处
<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
格式为*.xlsx / *.xls文件不超过一个
<el-link
type="primary"
icon="download"
underline="never"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
</template>
</el-upload>
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button v-if="resultData.length > 0" type="primary" @click="handleShowResult">
错误信息
</el-button>
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleUpload"
>
</el-button>
<el-button @click="handleClose"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="resultVisible" title="导入结果" width="600px">
<el-alert
:title="`导入结果:${invalidCount}条无效数据,${validCount}条有效数据`"
type="warning"
:closable="false"
/>
<el-table :data="resultData" style="width: 100%; max-height: 400px">
<el-table-column prop="index" align="center" width="100" type="index" label="序号" />
<el-table-column prop="message" label="错误信息" width="400">
<template #default="scope">
{{ scope.row }}
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCloseResult">关闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, type UploadUserFile } from "element-plus";
import UserAPI from "@/api/system/user-api";
import { ApiCodeEnum } from "@/enums/api/code-enum";
const emit = defineEmits(["import-success"]);
const visible = defineModel("modelValue", {
type: Boolean,
required: true,
default: false,
});
const resultVisible = ref(false);
const resultData = ref<string[]>([]);
const invalidCount = ref(0);
const validCount = ref(0);
const importFormRef = ref(null);
const uploadRef = ref(null);
const importFormData = reactive<{
files: UploadUserFile[];
}>({
files: [],
});
watch(visible, (newValue) => {
if (newValue) {
resultData.value = [];
resultVisible.value = false;
invalidCount.value = 0;
validCount.value = 0;
}
});
const importFormRules = {
files: [{ required: true, message: "文件不能为空", trigger: "blur" }],
};
// 文件超出个数限制
const handleFileExceed = () => {
ElMessage.warning("只能上传一个文件");
};
// 下载导入模板
const handleDownloadTemplate = () => {
UserAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
// 上传文件
const handleUpload = async () => {
if (!importFormData.files.length) {
ElMessage.warning("请选择文件");
return;
}
try {
const result = await UserAPI.import("1", importFormData.files[0].raw as File);
if (result.code === ApiCodeEnum.SUCCESS && result.invalidCount === 0) {
ElMessage.success("导入成功,导入数据:" + result.validCount + "条");
emit("import-success");
handleClose();
} else {
ElMessage.error("上传失败");
resultVisible.value = true;
resultData.value = result.messageList;
invalidCount.value = result.invalidCount;
validCount.value = result.validCount;
}
} catch (error: any) {
console.error(error);
ElMessage.error("上传失败:" + error);
}
};
// 显示错误信息
const handleShowResult = () => {
resultVisible.value = true;
};
// 关闭错误信息弹窗
const handleCloseResult = () => {
resultVisible.value = false;
};
// 关闭弹窗
const handleClose = () => {
importFormData.files.length = 0;
visible.value = false;
};
</script>

View File

@@ -0,0 +1,994 @@
<!-- 用户管理 -->
<template>
<div class="app-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.username"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<!-- <el-form-item label="状态" prop="status">-->
<!-- <el-select-->
<!-- v-model="queryParams.status"-->
<!-- placeholder="全部"-->
<!-- clearable-->
<!-- style="width: 100px"-->
<!-- >-->
<!-- <el-option label="正常" :value="1" />-->
<!-- <el-option label="禁用" :value="0" />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- <el-form-item label="创建时间">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- :editable="false"-->
<!-- type="daterange"-->
<!-- range-separator="~"-->
<!-- start-placeholder="开始时间"-->
<!-- end-placeholder="截止时间"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="hover" class="data-table">
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button
v-hasPerm="['sys:user:add']"
type="success"
icon="plus"
@click="handleOpenDialog()"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="data-table__toolbar--tools">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="pageData"
border
stripe
highlight-current-row
class="data-table__content"
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="account" />
<el-table-column label="昵称" align="center" prop="username" />
<!-- <el-table-column label="性别" width="100" align="center">-->
<!-- <template #default="scope">-->
<!-- <DictLabel v-model="scope.row.gender" code="gender" />-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="部门" align="center" prop="department" />
<el-table-column label="手机号码" align="center" prop="mobilePhone" />
<el-table-column label="入职时间" align="center" prop="Dateofjoining" />
<el-table-column label="操作" fixed="right" align="center" width="220">
<template #default="scope">
<!-- <el-button-->
<!-- v-hasPerm="'sys:user:reset-password'"-->
<!-- type="primary"-->
<!-- icon="RefreshLeft"-->
<!-- size="small"-->
<!-- link-->
<!-- @click="handleResetPassword(scope.row)"-->
<!-- >-->
<!-- 重置密码-->
<!-- </el-button>-->
<el-button
v-hasPerm="'sys:user:edit'"
type="primary"
icon="edit"
link
size="small"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchUserList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialog.visible"
:title="dialog.title"
append-to-body
:size="drawerSize"
@close="handleCloseDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="身份证号码" prop="IdCard">
<el-input v-model="formData.IdCard" placeholder="请输入身份证号码" maxlength="18" />
</el-form-item>
<el-form-item label="手机号" prop="mobilePhone">
<el-input v-model="formData.mobilePhone" placeholder="请输入手机号" maxlength="11" />
</el-form-item>
<el-form-item label="所属部门" prop="department">
<el-select v-model="formData.department" placeholder="请选择所属部门">
<el-option key="行政部" label="行政部" value="行政部" />
<el-option key="财务部" label="财务部" value="财务部" />
<el-option key="执业律师" label="执业律师" value="执业律师" />
<el-option key="实习律师" label="实习律师" value="实习律师" />
</el-select>
</el-form-item>
<el-form-item label="岗位" prop="position">
<el-select v-model="formData.position" placeholder="请选择所属部门">
<el-option key="助理" label="助理" value="助理" />
<el-option key="独立律师" label="独立律师" value="独立律师" />
<el-option key="一级主办律师" label="一级主办律师" value="一级主办律师" />
<el-option key="中级主办律师" label="中级主办律师" value="中级主办律师" />
<el-option key="高级主办律师" label="高级主办律师" value="高级主办律师" />
<el-option key="合伙人" label="合伙人" value="合伙人" />
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="team">
<el-select v-model="formData.team" placeholder="请选择所属团队">
<el-option key="团队一" label="团队一" value="团队一" />
<el-option key="团队二" label="团队二" value="团队二" />
<el-option key="团队三" label="团队三" value="团队三" />
</el-select>
</el-form-item>
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择执业证时间"
/>
</el-form-item>
<!-- <el-form-item label="离职时间" prop="resignationDate">-->
<!-- <el-date-picker-->
<!-- v-model="formData.resignationDate"-->
<!-- type="date"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- placeholder="请选择离职时间"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="学业简历" prop="academic">
<div style="width: 440px; display: flex; flex-direction: column">
<!-- 学业简历表格样式布局 -->
<div class="education-resume-table">
<!-- 表头 -->
<div class="education-resume-header">
<div class="table-col date-col">日期区间</div>
<div class="table-col school-col">毕业院校</div>
<div class="table-col major-col">专业</div>
<div class="table-col degree-col">学历</div>
<div class="table-col action-col">操作</div>
</div>
<!-- 表体 -->
<div
v-for="(item, index) in formData.academic"
:key="index"
class="education-resume-row"
>
<div class="table-col date-col">
<el-date-picker
v-model="item.education"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结业时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</div>
<div class="table-col school-col">
<el-input v-model="item.institute" placeholder="请输入毕业院校" />
</div>
<div class="table-col major-col">
<el-input v-model="item.major" placeholder="请输入专业" />
</div>
<div class="table-col degree-col">
<el-select v-model="item.educationLevel" placeholder="请选择学历">
<el-option key="高中" label="高中" value="高中" />
<el-option key="大专" label="大专" value="大专" />
<el-option key="本科" label="本科" value="本科" />
<el-option key="硕士" label="硕士" value="硕士" />
<el-option key="博士" label="博士" value="博士" />
</el-select>
</div>
<div class="table-col action-col">
<el-button type="danger" @click="handleDelete(index)">删除</el-button>
</div>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.AcademicResume" class="upload-preview">
<span>{{ formData.AcademicResume.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('AcademicResume')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contract" class="upload-preview">
<span>{{ formData.contract.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('contract')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.ApplicationForm" class="upload-preview">
<span>{{ formData.ApplicationForm.name }}</span>
<el-button type="danger" size="small" @click.stop="removeFile('ApplicationForm')">
删除
</el-button>
</div>
<el-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="handleCloseDialog">取 消</el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImport v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
// ==================== 1. Vue 核心 API ====================
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
// ==================== 2. Element Plus ====================
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserPageVO } from "@/api/system/user-api";
// ==================== 4. API 服务 ====================
import UserAPI from "@/api/system/user-api";
import DeptAPI from "@/api/system/dept-api";
import RoleAPI from "@/api/system/role-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
import { useUserStore } from "@/store";
// ==================== 6. Enums ====================
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useTableSelection } from "@/composables";
// ==================== 8. 组件 ====================
import DeptTree from "./components/DeptTree.vue";
import UserImport from "./components/UserImport.vue";
import {
UserCreateUser,
UserEditorialStaff,
UserPersonnelList,
} from "@/api/calibration/personnelManagement";
import { convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
name: "SystemUser",
inheritAttrs: false,
});
// ==================== Store 实例 ====================
const appStore = useAppStore();
const userStore = useUserStore();
// ==================== 响应式状态 ====================
// DOM 引用
const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialog = reactive({
visible: false,
title: "新增用户",
});
// 表单数据
const formData = reactive<any>({
account: "",
username: "",
password: "",
nation: "",
IdCard: "",
department: "",
mobilePhone: "",
position: "",
team: "",
Dateofjoining: "",
Confirmationtime: "",
Practicingcertificatetime: "",
AcademicResume: undefined,
academic: [
{
education: [],
institute: "",
major: "",
educationLevel: "",
},
],
contract: undefined,
ApplicationForm: undefined,
});
// 下拉选项数据
const deptOptions = ref<OptionType[]>();
const roleOptions = ref<OptionType[]>();
// 导入弹窗
const importDialogVisible = ref(false);
// ==================== 计算属性 ====================
/**
* 抽屉尺寸(响应式)
*/
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
// ==================== 表单验证规则 ====================
const rules = reactive({
username: [
{
required: true,
message: "用户名不能为空",
trigger: "blur",
},
],
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "密码不能为空",
trigger: "blur",
},
],
nation: [
{
required: true,
message: "民族不能为空",
trigger: "blur",
},
],
IdCard: [
{
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
department: [
{
required: true,
message: "归属部门不能为空",
trigger: "blur",
},
],
mobilePhone: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
position: [
{
required: true,
message: "岗位不能为空",
trigger: "blur",
},
],
team: [
{
required: true,
message: "所属团队不能为空",
trigger: "blur",
},
],
Dateofjoining: [
{
required: true,
message: "入职时间不能为空",
trigger: "blur",
},
],
academic: [
{
required: true,
message: "学业简历不能为空",
trigger: "blur",
},
],
AcademicResume: [
{
required: true,
message: "学历证明不能为空",
trigger: "blur",
},
],
contract: [
{
required: true,
message: "合同不能为空",
trigger: "blur",
},
],
ApplicationForm: [
{
required: true,
message: "入职申请表不能为空",
trigger: "blur",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取列表数据
*/
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const res: any = await UserPersonnelList(queryParams);
pageData.value = res.data;
total.value = res.total;
} catch (error) {
ElMessage.error("获取用户列表失败");
console.error("获取用户列表失败:", error);
} finally {
loading.value = false;
}
});
// ==================== 表格选择 ====================
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserPageVO>();
// ==================== 查询操作 ====================
/**
* 查询用户列表
*/
function handleQuery(): Promise<void> {
queryParams.pageNum = 1;
return fetchUserList();
}
/**
* 重置查询条件
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
/**
* 重置用户密码
* @param row 用户数据
*/
// function handleResetPassword(row: UserPageVO): void {
// ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
// confirmButtonText: "确定",
// cancelButtonText: "取消",
// inputPattern: /.{6,}/,
// inputErrorMessage: "密码至少需要6位字符",
// })
// .then(({ value }) => {
// return UserAPI.resetPassword(row.id, value);
// })
// .then(() => {
// ElMessage.success("密码重置成功");
// })
// .catch((error) => {
// if (error !== "cancel") {
// ElMessage.error("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
/**
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
try {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
} catch (error) {
ElMessage.error("加载选项数据失败");
console.error("加载选项数据失败:", error);
}
// 编辑:加载用户数据
if (data?.id) {
dialog.title = "修改用户";
try {
const data1 = deepCloneByJSON(data);
data1.academic = parseJsonToArray(data1.academic);
data1.AcademicResume = convertFilePathsToObject(JSON.parse(data1.AcademicResume))[0];
data1.contract = convertFilePathsToObject(JSON.parse(data1.contract))[0];
data1.ApplicationForm = convertFilePathsToObject(JSON.parse(data1.ApplicationForm))[0];
Object.assign(formData, data1);
} catch (error) {
ElMessage.error("加载用户数据失败");
console.error("加载用户数据失败:", error);
}
} else {
// 新增:设置默认值
dialog.title = "新增用户";
}
}
function deepCloneByJSON(obj: any) {
try {
return JSON.parse(JSON.stringify(obj));
} catch (error) {
console.error("深拷贝失败:", error);
return obj;
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: any) {
// 检查是否已经是对象
if (typeof jsonString === "object" && jsonString !== null) {
return Array.isArray(jsonString) ? jsonString : [jsonString];
}
// 检查是否为字符串
if (typeof jsonString === "string") {
try {
const parsed = JSON.parse(jsonString);
return Array.isArray(parsed) ? parsed : [parsed];
} catch (e) {
console.error("JSON解析失败:", e);
return [];
}
}
return [];
}
/**
* 关闭用户表单弹窗
*/
function handleCloseDialog(): void {
dialog.visible = false;
userFormRef.value.resetFields();
userFormRef.value.clearValidate();
// 重置表单数据
formData.id = undefined;
}
/**
* 提交用户表单(防抖)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value.validate().catch(() => false);
if (!valid) return;
const userId = formData.id;
loading.value = true;
try {
if (userId) {
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
handleResetQuery();
} catch (error) {
ElMessage.error(userId ? "修改用户失败" : "新增用户失败");
console.error("提交用户表单失败:", error);
} finally {
loading.value = false;
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
loading.value = true;
try {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleResetQuery();
} catch (error) {
ElMessage.error("删除失败");
console.error("删除用户失败:", error);
} finally {
loading.value = false;
}
})
.catch(() => {
// 用户取消操作,无需处理
});
}
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @param file 选择的文件对象
* @param field 表单字段名
*/
function handleFileSelect(file: any, field: string): void {
// 将文件对象保存到表单数据中
formData[field] = file.raw; // file.raw 是实际的 File 对象
console.log(formData[field]);
userFormRef.value.clearValidate(field);
ElMessage.success("文件选择成功");
}
/**
* 上传前的校验
* @param file 上传的文件对象
*/
function handleBeforeUpload(file: File): boolean {
// 可以在这里添加文件类型和大小的校验
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
ElMessage.error("上传文件大小不能超过 10MB!");
return false;
}
return true;
}
/**
* 处理文件上传成功后的回调
* @param res 上传响应数据
* @param file 上传的文件对象
* @param field 表单字段名
*/
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
* @param field 表单字段名
*/
function removeFile(field: string): void {
formData[field] = undefined;
ElMessage.success("文件已删除");
}
// ==================== 导入导出 ====================
/**
* 打开导入弹窗
*/
// function handleOpenImportDialog(): void {
// importDialogVisible.value = true;
// }
/**
* 导出用户列表
*/
async function handleExport(): Promise<void> {
try {
const response = await UserAPI.export(queryParams);
const fileData = response.data;
const contentDisposition = response.headers["content-disposition"];
const fileName = decodeURI(contentDisposition.split(";")[1].split("=")[1]);
const fileType =
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
// 创建下载链接
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
// 触发下载
document.body.appendChild(downloadLink);
downloadLink.click();
// 清理
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
ElMessage.success("导出成功");
} catch (error) {
ElMessage.error("导出失败");
console.error("导出用户列表失败:", error);
}
}
// ==================== 生命周期 ====================
/**
* 组件挂载时初始化数据
*
* 注意:这里会先加载列表数据,如果 URL 中有 AI 参数(如搜索关键字),
* useAiAction 会在 nextTick 中再次执行搜索,这是预期行为
*/
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
}
.education-resume-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #dcdfe6;
font-weight: bold;
padding: 8px 12px;
}
.education-resume-row {
display: flex;
border-bottom: 1px solid #ebeef5;
padding: 12px;
align-items: center;
}
.education-resume-row:last-child {
border-bottom: none;
}
.table-col {
display: flex;
align-items: center;
}
.date-col {
width: 25%;
}
.school-col {
width: 25%;
}
.major-col {
width: 20%;
}
.degree-col {
width: 20%;
}
.action-col {
width: 10%;
justify-content: center;
}
.education-resume-actions {
margin-top: 12px;
display: flex;
justify-content: flex-start;
}
/* 表单元素样式调整 */
.education-resume-row .el-input,
.education-resume-row .el-select,
.education-resume-row .el-date-editor {
width: 100%;
}
</style>