登录接口对接、人事管理接口对接

This commit is contained in:
雷校云
2025-12-09 18:07:41 +08:00
parent a66ee9b943
commit b576a787aa
13 changed files with 727 additions and 288 deletions

View File

@@ -6,7 +6,7 @@ const AuthAPI = {
/** 登录接口*/
login(data: LoginFormData) {
const formData = new FormData();
formData.append("username", data.username);
formData.append("username", "admin");
formData.append("password", data.password);
formData.append("captchaKey", data.captchaKey);
formData.append("captchaCode", data.captchaCode);

View File

@@ -0,0 +1,58 @@
import request from "@/utils/request";
const AUTH_BASE_URL = "/api2";
// 公司部门列表
export const UserDepartment = (name: string) => {
const formData = new FormData();
formData.append("name", name);
return request({
url: `${AUTH_BASE_URL}/user/department`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 添加部门
export const UserAddDepartment = (name: string) => {
const formData = new FormData();
formData.append("name", name);
return request({
url: `${AUTH_BASE_URL}/user/add_department`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 删除部门
export const UserDeleteDepartment = (name: string) => {
const formData = new FormData();
formData.append("name", name);
return request({
url: `${AUTH_BASE_URL}/user/delete_department`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 公司部门人员名单
export const UserPersonlist = (name: string) => {
const formData = new FormData();
formData.append("name", name);
return request({
url: `${AUTH_BASE_URL}/user/personlist`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};

View File

@@ -0,0 +1,31 @@
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) => {
const formData = new FormData();
formData.append("username", data.username);
formData.append("password", data.password);
return request({
url: `${AUTH_BASE_URL}/user/login`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 人员展示接口
export const UserGetInfo = (account: string) => {
const formData = new FormData();
formData.append("account", account);
return request({
url: `${AUTH_BASE_URL}/user/get_info`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};

View File

@@ -0,0 +1,93 @@
import request from "@/utils/request";
const AUTH_BASE_URL = "/api2";
// 人员列表
export const UserPersonnelList = (data: any) => {
const formData = new FormData();
formData.append("page", data.pageNum);
formData.append("per_page", data.pageSize);
formData.append("username", data.username);
formData.append("department", data.department);
return request({
url: `${AUTH_BASE_URL}/user/personnel-list`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 人事管理-人员添加
export const UserCreateUser = (data: any) => {
const formData = new FormData();
formData.append("username", data.username);
formData.append("account", data.account);
formData.append("password", data.password);
formData.append("nation", data.nation);
formData.append("IdCard", data.IdCard);
formData.append("department", data.department);
formData.append("mobilePhone", data.mobilePhone);
formData.append("position", data.position);
formData.append("team", data.team);
formData.append("Dateofjoining", data.Dateofjoining);
formData.append("Confirmationtime", data.Confirmationtime);
formData.append("Practicingcertificatetime", data.Practicingcertificatetime);
formData.append("AcademicResume", data.AcademicResume);
formData.append("academic", JSON.stringify(data.academic));
formData.append("contract", data.contract);
formData.append("ApplicationForm", data.ApplicationForm);
return request({
url: `${AUTH_BASE_URL}/user/create-user`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 人事管理-人员编辑
export const UserEditorialStaff = (data: any) => {
const formData = new FormData();
formData.append("id", data.id);
formData.append("username", data.username);
formData.append("account", data.account);
formData.append("password", data.password);
formData.append("nation", data.nation);
formData.append("IdCard", data.IdCard);
formData.append("department", data.department);
formData.append("mobilePhone", data.mobilePhone);
formData.append("position", data.position);
formData.append("team", data.team);
formData.append("Dateofjoining", data.Dateofjoining);
formData.append("Confirmationtime", data.Confirmationtime);
formData.append("Practicingcertificatetime", data.Practicingcertificatetime);
formData.append("AcademicResume", data.AcademicResume);
formData.append("academic", JSON.stringify(data.academic));
formData.append("contract", data.contract);
formData.append("ApplicationForm", data.ApplicationForm);
return request({
url: `${AUTH_BASE_URL}/user/editorial-staff`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};
// 人员展示接口
export const UserPersonnelDetails = (data: any) => {
const formData = new FormData();
formData.append("account", data.account);
return request({
url: `${AUTH_BASE_URL}/user/personnel-details`,
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
});
};

View File

@@ -87,7 +87,7 @@ const UserAPI = {
*
* @param ids 用户ID字符串多个以英文逗号(,)分割
*/
deleteByIds(ids: string) {
deleteByIds(ids: string | number) {
return request({
url: `${USER_BASE_URL}/${ids}`,
method: "delete",

View File

@@ -1,6 +1,6 @@
<template>
<el-dropdown trigger="click">
<el-badge v-if="noticeList.length > 0" :value="noticeList.length" :max="99">
<el-badge v-if="noticeList?.length > 0" :value="noticeList?.length" :max="99">
<div class="i-svg:bell" />
</el-badge>
@@ -8,7 +8,7 @@
<template #dropdown>
<div class="p-5">
<template v-if="noticeList.length > 0">
<template v-if="noticeList?.length > 0">
<div v-for="(item, index) in noticeList" :key="index" class="w-500px py-3">
<div class="flex-y-center">
<DictLabel v-model="item.type" code="notice_type" size="small" />
@@ -35,7 +35,7 @@
</el-icon>
</el-link>
<el-link
v-if="noticeList.length > 0"
v-if="noticeList?.length > 0"
type="primary"
underline="never"
@click="handleMarkAllAsRead"

View File

@@ -8,6 +8,7 @@ import { usePermissionStoreHook } from "@/store/modules/permission-store";
import { useDictStoreHook } from "@/store/modules/dict-store";
import { useTagsViewStore } from "@/store";
import { cleanupWebSocket } from "@/plugins/websocket";
import { userLogin } from "@/api/calibration/login";
export const useUserStore = defineStore("user", () => {
// 用户信息
@@ -24,12 +25,19 @@ export const useUserStore = defineStore("user", () => {
function login(LoginFormData: LoginFormData) {
return new Promise<void>((resolve, reject) => {
AuthAPI.login(LoginFormData)
.then((data) => {
.then(({ data }: any) => {
const { accessToken, refreshToken } = data;
// 保存记住我状态和token
rememberMe.value = LoginFormData.rememberMe;
AuthStorage.setTokens(accessToken, refreshToken, rememberMe.value);
resolve();
userLogin(LoginFormData)
.then(() => {
console.log("login success");
resolve();
})
.catch((error) => {
reject(error);
});
})
.catch((error) => {
reject(error);

View File

@@ -0,0 +1,141 @@
/**
* 节流函数 - 限制函数执行频率
* @param func 需要节流的函数
* @param delay 节流延迟时间(毫秒)
* @returns 节流后的函数
*/
export function throttle<T extends (...args: any[]) => any>(
func: T,
delay: number
): (...args: Parameters<T>) => void {
let lastExecTime = 0;
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
const now = Date.now();
// 如果距离上次执行时间超过延迟时间,则立即执行
if (now - lastExecTime >= delay) {
func.apply(this, args);
lastExecTime = now;
} else {
// 否则延迟执行,避免频繁触发
if (timer) clearTimeout(timer);
timer = setTimeout(
() => {
func.apply(this, args);
lastExecTime = Date.now();
timer = null;
},
delay - (now - lastExecTime)
);
}
};
}
// 处理后端返回的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 文件路径数组或单个文件路径字符串
* @returns 包含文件信息的对象数组
*/
export function convertFilePathsToObject(filePaths: string | string[]): Array<{
url: string;
name: string;
domain: string;
uuid: string;
}> {
// 如果是字符串,先转换为数组
const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
return paths.map((path) => {
// 确保path是字符串类型
if (typeof path !== "string") {
return {
url: "",
domain: "",
name: "",
uuid: "",
};
}
// 分割域名和文件部分
const parts = path.split("/");
const domain = parts[0] || "";
const filePart = parts.slice(1).join("/") || "";
// 分割文件名和UUID
const lastUnderscoreIndex = filePart.lastIndexOf("_");
const name =
lastUnderscoreIndex > 0 ? filePart.substring(0, lastUnderscoreIndex) : filePart;
const uuid = lastUnderscoreIndex > 0 ? filePart.substring(lastUnderscoreIndex + 1) : "";
return {
url: path,
domain,
name,
uuid,
};
});
}

View File

@@ -8,6 +8,16 @@ import { authConfig } from "@/settings";
// 初始化token刷新组合式函数
const { refreshTokenAndRetry } = useTokenRefresh();
// 生成随机Token
const generateRandomToken = () => {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let token = "";
for (let i = 0; i < 32; i++) {
token += characters.charAt(Math.floor(Math.random() * characters.length));
}
return token;
};
/**
* 创建 HTTP 请求实例
*/
@@ -24,7 +34,6 @@ const httpRequest = axios.create({
httpRequest.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const accessToken = AuthStorage.getAccessToken();
// 如果 Authorization 设置为 no-auth则不携带 Token
if (config.headers.Authorization !== "no-auth" && accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
@@ -50,11 +59,10 @@ httpRequest.interceptors.response.use(
return response;
}
const { code, data, msg } = response.data;
const { code, msg } = response.data;
// 请求成功
if (code === ApiCodeEnum.SUCCESS) {
return data;
if (code === ApiCodeEnum.SUCCESS || code == "0") {
return response.data as any;
}
// 业务错误

View File

@@ -52,7 +52,7 @@
<div cursor-pointer h-40px w-120px flex-center @click="getCaptcha">
<el-icon v-if="codeLoading" class="is-loading" size="20"><Loading /></el-icon>
<img
v-else-if="captchaBase64"
v-else-if="captchaBase64 && captchaBase64.length > 0"
border-rd-4px
object-cover
shadow="[0_0_0_1px_var(--el-border-color)_inset]"
@@ -89,7 +89,6 @@ import type { FormInstance } from "element-plus";
import AuthAPI, { type LoginFormData } from "@/api/auth-api";
import router from "@/router";
import { useUserStore } from "@/store";
import CommonWrapper from "@/components/CommonWrapper/index.vue";
import { AuthStorage } from "@/utils/auth";
const { t } = useI18n();
@@ -151,7 +150,7 @@ const codeLoading = ref(false);
function getCaptcha() {
codeLoading.value = true;
AuthAPI.getCaptcha()
.then((data) => {
.then(({ data }: any) => {
loginFormData.value.captchaKey = data.captchaKey;
captchaBase64.value = data.captchaBase64;
})

View File

@@ -1,7 +1,7 @@
<!-- 部门树 -->
<template>
<el-card shadow="never">
<el-input v-model="deptName" placeholder="部门名称" clearable>
<el-input v-model="deptName" placeholder="部门名称" clearable @input="deptNameInput">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
@@ -11,7 +11,10 @@
ref="deptTreeRef"
class="mt-2"
:data="deptList"
:props="{ children: 'children', label: 'label', disabled: '' }"
: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
@@ -21,7 +24,8 @@
</template>
<script setup lang="ts">
import DeptAPI from "@/api/system/dept-api";
import { UserDepartment } from "@/api/calibration/department";
import { throttle } from "@/utils/auxiliaryFunction";
const props = defineProps({
modelValue: {
type: [String, Number],
@@ -29,23 +33,26 @@ const props = defineProps({
},
});
const deptList = ref<OptionType[]>(); // 部门列表
const deptList = ref<any[]>(); // 部门列表
const deptTreeRef = ref(); // 部门树
const deptName = ref(); // 部门名称
const deptName = ref(""); // 部门名称
const emits = defineEmits(["node-click"]);
const emits = defineEmits(["node-click", "update:modelValue"]);
const deptId = useVModel(props, "modelValue", emits);
watchEffect(
() => {
deptTreeRef.value.filter(deptName.value);
},
{
flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
}
);
// watchEffect(
// () => {
// deptTreeRef.value.filter(deptName.value);
// },
// {
// flush: "post", // watchEffect会在DOM挂载或者更新之前就会触发此属性控制在DOM元素更新后运行
// }
// );
const deptNameInput = throttle((value: string) => {
DepartmentList(value);
}, 300);
/**
* 部门筛选
*/
@@ -58,13 +65,19 @@ function handleFilter(value: string, data: any) {
/** 部门树节点 Click */
function handleNodeClick(data: { [key: string]: any }) {
deptId.value = data.value;
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(() => {
DeptAPI.getOptions().then((data) => {
deptList.value = data;
});
DepartmentList();
});
</script>

View File

@@ -4,7 +4,7 @@
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="mb-[12px]">
<DeptTree v-model="queryParams.deptId" @node-click="handleQuery" />
<DeptTree v-model="queryParams.department" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
@@ -14,36 +14,36 @@
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.keywords"
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="状态" 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 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>
@@ -74,13 +74,13 @@
</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:import'"-->
<!-- icon="upload"-->
<!-- @click="handleOpenImportDialog"-->
<!-- >-->
<!-- 导入-->
<!-- </el-button>-->
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="handleExport">
导出
@@ -99,43 +99,35 @@
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="username" />
<el-table-column label="昵称" width="150" align="center" prop="nickname" />
<el-table-column label="性别" width="100" 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">
<DictLabel v-model="scope.row.gender" code="gender" />
</template>
</el-table-column>
<el-table-column label="部门" width="120" align="center" prop="deptName" />
<el-table-column label="手机号码" align="center" prop="mobile" width="120" />
<el-table-column label="邮箱" align="center" prop="email" width="160" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<el-tag :type="scope.row.status == 1 ? 'success' : 'info'">
{{ scope.row.status == 1 ? "正常" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150" />
<el-table-column label="操作" fixed="right" 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: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.id)"
@click="handleOpenDialog(scope.row)"
>
编辑
</el-button>
@@ -174,53 +166,33 @@
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="姓名" prop="username">
<el-input
v-model="formData.username"
:readonly="!!formData.id"
placeholder="请输入姓名"
/>
<el-input v-model="formData.username" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="民族" prop="nationality">
<el-input
v-model="formData.nationality"
:readonly="!!formData.nationality"
placeholder="请输入民族"
/>
<el-form-item label="账号" prop="account">
<el-input v-model="formData.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="身份证号码" prop="idCard">
<el-input
v-model="formData.idCard"
:readonly="!!formData.idCard"
placeholder="请输入身份证号码"
maxlength="18"
/>
<el-form-item label="码" prop="password">
<el-input v-model="formData.password" placeholder="请输入密码" />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input
v-model="formData.mobile"
:readonly="!!formData.mobile"
placeholder="请输入手机号"
maxlength="11"
/>
<el-form-item label="民族" prop="nation">
<el-input v-model="formData.nation" placeholder="请输入民族" />
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<el-select
v-model="formData.deptId"
placeholder="请选择所属部门"
:readonly="!!formData.deptId"
>
<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="roleIds">
<el-select
v-model="formData.roleIds"
placeholder="请选择所属部门"
:readonly="!!formData.roleIds"
>
<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="一级主办律师" />
@@ -230,158 +202,151 @@
<el-option key="已离职" label="已离职" value="已离职" />
</el-select>
</el-form-item>
<el-form-item label="所属团队" prop="teamIds">
<el-select
v-model="formData.teamIds"
placeholder="请选择所属团队"
:readonly="!!formData.teamIds"
>
<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="hiredDate">
<el-form-item label="入职时间" prop="Dateofjoining">
<el-date-picker
v-model="formData.hiredDate"
v-model="formData.Dateofjoining"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择入职时间"
/>
</el-form-item>
<el-form-item label="转正时间" prop="promotionDate">
<el-form-item label="转正时间" prop="Confirmationtime">
<el-date-picker
v-model="formData.promotionDate"
v-model="formData.Confirmationtime"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择转正时间"
/>
</el-form-item>
<el-form-item label="执业证时间" prop="licenseDate">
<el-form-item label="执业证时间" prop="Practicingcertificatetime">
<el-date-picker
v-model="formData.licenseDate"
v-model="formData.Practicingcertificatetime"
type="date"
value-format="YYYY-MM"
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="educationResume">
<!-- 学业简历表格样式布局 -->
<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>
<!-- <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
v-for="(item, index) in formData.educationResume"
: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 class="education-resume-actions">
<el-button type="primary" @click="AddEducationalBackground">添加教育经历</el-button>
</div>
</div>
<!-- 添加按钮 -->
<div class="education-resume-actions">
<el-button type="primary" @click="handleAddEducationResume">添加教育经历</el-button>
</div>
</el-form-item>
<el-form-item label="学历证书" prop="educationLevel">
<el-form-item label="学历证书" prop="AcademicResume">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-success="(res, file: File) => handleFileUploadSuccess(res, file, 'educationLevel')"
:on-change="(file) => handleFileSelect(file, 'AcademicResume')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.educationLevel" class="upload-preview">
<el-link :href="formData.educationLevel" target="_blank" type="primary">
查看学历证书
</el-link>
<el-button type="danger" size="small" @click.stop="removeFile('educationLevel')">
<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-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="劳动合同" prop="contractResume">
<el-form-item label="劳动合同" prop="contract">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-success="(res, file: File) => handleFileUploadSuccess(res, file, 'contractResume')"
:on-change="(file) => handleFileSelect(file, 'contract')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.contractResume" class="upload-preview">
<el-link :href="formData.contractResume" target="_blank" type="primary">
查看劳动合同
</el-link>
<el-button type="danger" size="small" @click.stop="removeFile('contractResume')">
<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-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
<el-form-item label="入职申请表" prop="hiredResume">
<el-form-item label="入职申请表" prop="ApplicationForm">
<el-upload
class="avatar-uploader"
action="#"
:auto-upload="false"
:show-file-list="false"
:on-success="
(res: any, file: File) => handleFileUploadSuccess(res, file, 'hiredResume')
"
:on-change="(file) => handleFileSelect(file, 'ApplicationForm')"
:before-upload="handleBeforeUpload"
>
<div v-if="formData.hiredResume" class="upload-preview">
<el-link :href="formData.hiredResume" target="_blank" type="primary">
查看入职申请表
</el-link>
<el-button type="danger" size="small" @click.stop="removeFile('hiredResume')">
<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-button v-else size="small" type="primary">点击选择文件</el-button>
</el-upload>
</el-form-item>
</el-form>
@@ -407,12 +372,11 @@ import { useDebounceFn } from "@vueuse/core";
import { ElMessage, ElMessageBox } from "element-plus";
// ==================== 3. 类型定义 ====================
import type { UserForm, UserPageQuery, UserPageVO } from "@/api/system/user-api";
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";
import fileApi from "@/api/file-api";
// ==================== 5. Store ====================
import { useAppStore } from "@/store/modules/app-store";
@@ -422,11 +386,17 @@ import { useUserStore } from "@/store";
import { DeviceEnum } from "@/enums/settings/device-enum";
// ==================== 7. Composables ====================
import { useAiAction, useTableSelection } from "@/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 { formatJsonString, convertFilePathsToObject } from "@/utils/auxiliaryFunction";
// ==================== 组件配置 ====================
defineOptions({
@@ -445,13 +415,15 @@ const queryFormRef = ref();
const userFormRef = ref();
// 列表查询参数
const queryParams = reactive<UserPageQuery>({
const queryParams = reactive<any>({
pageNum: 1,
pageSize: 10,
department: undefined,
username: "",
});
// 列表数据
const pageData = ref<UserPageVO[]>();
const pageData = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);
@@ -462,18 +434,30 @@ const dialog = reactive({
});
// 表单数据
const formData = reactive<UserForm>({
educationResume: [
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,
});
// 下拉选项数据
@@ -500,68 +484,120 @@ const rules = reactive({
trigger: "blur",
},
],
nickname: [
account: [
{
required: true,
message: "用户昵称不能为空",
trigger: "blur",
},
],
deptId: [
password: [
{
required: true,
message: "所属部门不能为空",
message: "密码不能为空",
trigger: "blur",
},
],
roleIds: [
nation: [
{
required: true,
message: "用户角色不能为空",
message: "民族不能为空",
trigger: "blur",
},
],
email: [
IdCard: [
{
pattern: /\w[-\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\.)+[A-Za-z]{2,14}/,
message: "请输入正确的邮箱地址",
required: true,
message: "身份证不能为空",
trigger: "blur",
},
],
mobile: [
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",
},
],
educationResume: [
position: [
{
required: true,
message: "请输入教育经历",
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",
},
],
});
// ==================== 数据加载 ====================
/**
* 获取用户列表数据
* 获取列表数据
*/
async function fetchUserList(): Promise<void> {
const fetchUserList = useDebounceFn(async () => {
if (!queryParams.department) return;
loading.value = true;
try {
const data = await UserAPI.getPage(queryParams);
pageData.value = data.list;
total.value = data.total;
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>();
@@ -581,8 +617,7 @@ function handleQuery(): Promise<void> {
*/
function handleResetQuery(): void {
queryFormRef.value.resetFields();
queryParams.deptId = undefined;
queryParams.createTime = undefined;
queryParams.username = "";
handleQuery();
}
// ==================== 用户操作 ====================
@@ -591,25 +626,25 @@ function handleResetQuery(): void {
* 重置用户密码
* @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("密码重置失败");
}
});
}
// 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("密码重置失败");
// }
// });
// }
// ==================== 弹窗操作 ====================
@@ -617,7 +652,7 @@ function handleResetPassword(row: UserPageVO): void {
* 打开用户表单弹窗
* @param id 用户ID编辑时传入
*/
async function handleOpenDialog(id?: string): Promise<void> {
async function handleOpenDialog(data?: any): Promise<void> {
dialog.visible = true;
// 并行加载下拉选项数据
@@ -632,10 +667,15 @@ async function handleOpenDialog(id?: string): Promise<void> {
}
// 编辑:加载用户数据
if (id) {
if (data?.id) {
dialog.title = "修改用户";
try {
const data = await UserAPI.getFormData(id);
data.academic = parseJsonToArray(data.academic);
data.AcademicResume = convertFilePathsToObject(JSON.parse(data.AcademicResume))[0];
data.contract = convertFilePathsToObject(JSON.parse(data.contract))[0];
data.ApplicationForm = convertFilePathsToObject(JSON.parse(data.ApplicationForm))[0];
console.log(data);
Object.assign(formData, data);
} catch (error) {
ElMessage.error("加载用户数据失败");
@@ -647,6 +687,17 @@ async function handleOpenDialog(id?: string): Promise<void> {
}
}
// 将 JSON 字符串转换为数组对象
function parseJsonToArray(jsonString: string): any[] {
try {
const result = JSON.parse(jsonString);
return Array.isArray(result) ? result : [];
} catch (error) {
console.error("JSON解析失败:", error);
return [];
}
}
/**
* 关闭用户表单弹窗
*/
@@ -671,10 +722,10 @@ const handleSubmit = useDebounceFn(async () => {
try {
if (userId) {
await UserAPI.update(userId, formData);
await UserEditorialStaff(formData);
ElMessage.success("修改用户成功");
} else {
await UserAPI.create(formData);
await UserCreateUser(formData);
ElMessage.success("新增用户成功");
}
handleCloseDialog();
@@ -687,11 +738,20 @@ const handleSubmit = useDebounceFn(async () => {
}
}, 1000);
const AddEducationalBackground = () => {
formData.academic.push({
education: [],
institute: "",
major: "",
educationLevel: "",
});
};
/**
* 删除用户
* @param id 用户ID单个删除时传入
*/
function handleDelete(id?: string): void {
function handleDelete(id?: string | number): void {
const userIds = id ? id : selectedIds.value.join(",");
if (!userIds) {
@@ -737,6 +797,19 @@ function handleDelete(id?: string): void {
// ==================== 上传文件 ====================
/**
* 处理文件选择
* @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 上传的文件对象
@@ -757,11 +830,11 @@ function handleBeforeUpload(file: File): boolean {
* @param file 上传的文件对象
* @param field 表单字段名
*/
function handleFileUploadSuccess(res: any, file: File, field: string): void {
formData[field] = res.data.url;
userFormRef.value.clearValidate(field);
ElMessage.success("文件上传成功");
}
// function handleFileUploadSuccess(res: any, file: File, field: string): void {
// formData[field] = res.data.url;
// userFormRef.value.clearValidate(field);
// ElMessage.success("文件上传成功");
// }
/**
* 删除已上传的文件
@@ -826,11 +899,18 @@ async function handleExport(): Promise<void> {
onMounted(() => {
handleQuery();
});
watch(
() => queryParams.department,
() => {
fetchUserList();
}
);
</script>
<!-- 学业简历表格样式 -->
<style scoped>
.education-resume-table {
flex: 1;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;

View File

@@ -44,13 +44,21 @@ export default defineConfig(({ mode }: ConfigEnv) => {
port: +(env.VITE_APP_PORT as string),
open: true,
proxy: {
"/dev-api/api2": {
changeOrigin: true,
target: "http://8.137.99.82:8006",
rewrite: (path: string) => {
return path.replace(/^\/dev-api\/api2/, "");
},
},
// 代理 /dev-api 的请求
[env.VITE_APP_BASE_API as string]: {
changeOrigin: true,
// 代理目标地址https://api.youlai.tech
target: env.VITE_APP_API_URL as string,
rewrite: (path: string) =>
path.replace(new RegExp("^" + (env.VITE_APP_BASE_API as string)), ""),
rewrite: (path: string) => {
return path.replace(new RegExp("^" + (env.VITE_APP_BASE_API as string)), "");
},
},
},
},