菜单页面文件创建、路由配置、入职财务登记api实现、开票申请api实现
This commit is contained in:
@@ -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);
|
||||
|
||||
43
src/api/calibration/invoiceApplication/index.ts
Normal file
43
src/api/calibration/invoiceApplication/index.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
25
src/api/calibration/onboardingRegistration/index.ts
Normal file
25
src/api/calibration/onboardingRegistration/index.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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: "工资/奖金变更",
|
||||
},
|
||||
|
||||
@@ -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 文件路径数组或单个文件路径字符串
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
994
src/views/calibration/accountAdjustmentApplication/index.vue
Normal file
994
src/views/calibration/accountAdjustmentApplication/index.vue
Normal 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>
|
||||
444
src/views/calibration/department/index.vue
Normal file
444
src/views/calibration/department/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
994
src/views/calibration/departureFinancialRegistration/index.vue
Normal file
994
src/views/calibration/departureFinancialRegistration/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
994
src/views/calibration/invoiceApplication/index.vue
Normal file
994
src/views/calibration/invoiceApplication/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
994
src/views/calibration/onboardingRegistration/index.vue
Normal file
994
src/views/calibration/onboardingRegistration/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
994
src/views/calibration/paymentApplicationForm/index.vue
Normal file
994
src/views/calibration/paymentApplicationForm/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
1024
src/views/calibration/personnelManagement/index.vue
Normal file
1024
src/views/calibration/personnelManagement/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
83
src/views/calibration/reimbursement/components/DeptTree.vue
Normal file
83
src/views/calibration/reimbursement/components/DeptTree.vue
Normal 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>
|
||||
198
src/views/calibration/reimbursement/components/UserImport.vue
Normal file
198
src/views/calibration/reimbursement/components/UserImport.vue
Normal 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>
|
||||
994
src/views/calibration/reimbursement/index.vue
Normal file
994
src/views/calibration/reimbursement/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
994
src/views/calibration/revenueRecognition/index.vue
Normal file
994
src/views/calibration/revenueRecognition/index.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
994
src/views/calibration/salaryBonusAdjustment/index.vue
Normal file
994
src/views/calibration/salaryBonusAdjustment/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user