995 lines
30 KiB
Vue
995 lines
30 KiB
Vue
<!-- 用户管理 -->
|
||
<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>
|