Compare commits
2 Commits
6c904ef3cd
...
4cbb93b19a
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cbb93b19a | |||
| 9669766eb3 |
3
auto-imports.d.ts
vendored
3
auto-imports.d.ts
vendored
@ -6,5 +6,6 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
|
||||
}
|
||||
|
||||
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -47,7 +47,6 @@ declare module 'vue' {
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
|
||||
@ -10,7 +10,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getOrganizationList() {
|
||||
return request({
|
||||
url: '/admin/erp/organization',
|
||||
url: '/admin/erp/getOrganization',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
@ -34,7 +34,7 @@ export function getOrganizationDetail(id) {
|
||||
*/
|
||||
export function createOrganization(data) {
|
||||
return request({
|
||||
url: "/admin/erp/organization",
|
||||
url: "/admin/erp/createOrganization",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
@ -46,7 +46,7 @@ export function createOrganization(data) {
|
||||
// 更新组织机构信息
|
||||
export function editOrganization(id, data) {
|
||||
return request({
|
||||
url: `/admin/erp/organization/${id}`,
|
||||
url: `/admin/erp/editOrganization/${id}`,
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
@ -59,7 +59,7 @@ export function editOrganization(id, data) {
|
||||
*/
|
||||
export function deleteOrganization(id) {
|
||||
return request({
|
||||
url: `/admin/erp/organization/${id}`,
|
||||
url: `/admin/erp/deleteOrganization/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
84
src/api/tenant.js
Normal file
84
src/api/tenant.js
Normal file
@ -0,0 +1,84 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/*************************************************
|
||||
****************** 租户相关接口 ******************
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* 获取租户列表
|
||||
* @param {Object} params 包含 page 和 pageSize
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTenantList(params) {
|
||||
return request({
|
||||
url: "/admin/tenant/getTenant",
|
||||
method: "get",
|
||||
params: params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户详情
|
||||
* @param {number} id 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTenantDetail(id) {
|
||||
return request({
|
||||
url: `/admin/tenant/getTenantDetail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户数据
|
||||
* @param {Object} data 租户数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createTenant(data) {
|
||||
return request({
|
||||
url: "/admin/tenant/createTenant",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户数据
|
||||
* @param {Object} data 租户数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function editTenant(id, data) {
|
||||
return request({
|
||||
url: `/admin/tenant/editTenant/${id}`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户数据
|
||||
* @param {number} id 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteTenant(id) {
|
||||
return request({
|
||||
url: `/admin/tenant/deleteTenant/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验租户编码是否重复
|
||||
* @param {string} tenant_code 编码
|
||||
* @param {number} id 可选,当前编辑的租户ID
|
||||
*/
|
||||
export function checkTenantCode(tenant_code) {
|
||||
return request({
|
||||
url: '/admin/tenant/findTenantCode',
|
||||
method: 'get',
|
||||
params: { tenant_code }
|
||||
});
|
||||
}
|
||||
409
src/views/apps/erp/employee/components/edit.vue
Normal file
409
src/views/apps/erp/employee/components/edit.vue
Normal file
@ -0,0 +1,409 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<div class="form-title">账号信息</div>
|
||||
<!-- 账号 -->
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-form-item label="密码" prop="password" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<div class="form-title">个人信息</div>
|
||||
<!-- 姓名 -->
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 电话 -->
|
||||
<el-form-item label="电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- QQ -->
|
||||
<el-form-item label="QQ">
|
||||
<el-input v-model="form.qq" placeholder="请输入QQ" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 性别 -->
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
||||
<el-radio-button label="男" :value="1" />
|
||||
<el-radio-button label="女" :value="2" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { addUser, editUser, getUserInfo } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "submit", "close"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 1,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return isAdd.value ? "添加用户" : "编辑用户";
|
||||
});
|
||||
|
||||
// 密码验证规则
|
||||
const validatePassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须符合规则
|
||||
if (value && value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 确认密码验证规则
|
||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,确认密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则确认密码必须一致
|
||||
if (form.value.password && value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
// 如果填写了确认密码但没填密码,提示错误
|
||||
if (value && !form.value.password) {
|
||||
callback(new Error("请先输入密码"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
account: [
|
||||
{ required: true, message: "请输入账号", trigger: "blur" },
|
||||
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
password: [{ validator: validatePassword, trigger: "blur" }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
|
||||
};
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 statusDict 变化,用于调试
|
||||
watch(
|
||||
() => props.statusDict,
|
||||
(newVal) => {},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
// 处理两种调用方式:传递用户对象或用户 ID
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getUserInfo(userId);
|
||||
|
||||
const data = res.data || res;
|
||||
|
||||
// 确保 sex 和 status 都是数字类型
|
||||
const sexValue =
|
||||
data.sex !== undefined && data.sex !== null
|
||||
? Number(data.sex)
|
||||
: 1;
|
||||
|
||||
const statusValue =
|
||||
data.status !== undefined && data.status !== null
|
||||
? Number(data.status)
|
||||
: 1;
|
||||
|
||||
form.value = {
|
||||
id: data.id,
|
||||
account: data.account,
|
||||
name: data.name,
|
||||
phone: data.phone,
|
||||
qq: data.qq,
|
||||
sex: sexValue,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: data.email,
|
||||
status: statusValue,
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error("Failed to load user data:", e);
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (error) {
|
||||
ElMessage.warning("请检查表单填写是否正确");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码一致性
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!form.value.password) {
|
||||
ElMessage.error("请输入密码");
|
||||
return;
|
||||
}
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须填写确认密码且一致
|
||||
if (form.value.password) {
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isAdd.value) {
|
||||
// 新增用户
|
||||
const submitData: any = {
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
qq: form.value.qq,
|
||||
sex: form.value.sex,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
password: form.value.password,
|
||||
};
|
||||
|
||||
await addUser(submitData);
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
// 编辑用户
|
||||
if (!form.value.id || form.value.id === 0) {
|
||||
ElMessage.error("用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData: any = {
|
||||
id: form.value.id,
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
qq: form.value.qq,
|
||||
sex: form.value.sex,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
// 只有在填写了密码时才添加到提交数据中
|
||||
if (form.value.password) {
|
||||
submitData.password = form.value.password;
|
||||
}
|
||||
|
||||
await editUser(form.value.id, submitData);
|
||||
ElMessage.success("更新成功");
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
emit("submit");
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
ElMessage.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openAdd: () => {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 1,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
};
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
openEdit: (user: any) => {
|
||||
isAdd.value = false;
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
// 异步加载用户详细信息
|
||||
loadUserData(user);
|
||||
},
|
||||
open: (user?: any) => {
|
||||
if (user) {
|
||||
isAdd.value = false;
|
||||
loadUserData(user);
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 1,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
0
src/views/apps/erp/employee/components/view.vue
Normal file
0
src/views/apps/erp/employee/components/view.vue
Normal file
94
src/views/apps/erp/employee/index.vue
Normal file
94
src/views/apps/erp/employee/index.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
||||
<el-table-column prop="account" label="账号" align="center" />
|
||||
<el-table-column prop="name" label="姓名" align="center">
|
||||
<template #default="scope">
|
||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="qq" label="隶属单位" align="center" />
|
||||
<el-table-column prop="phone" label="部门" align="center" />
|
||||
<el-table-column prop="phone" label="职位" align="center" />
|
||||
<el-table-column prop="status" label="账号状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
||||
scope.row.status === 1 ? "启用" : "禁用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button v-if="scope.row.username !== 'admin' && scope.row.id !== 1" size="small" type="danger"
|
||||
@click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
|
||||
layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -65,12 +65,10 @@ interface TreeNode {
|
||||
sort?: number
|
||||
status?: number
|
||||
remark?: string
|
||||
children?: TreeNode[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
treeData: TreeNode[]
|
||||
treeProps: { label: string; children: string; value: string }
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -114,9 +112,16 @@ const getLeaderList = async () => {
|
||||
// 打开弹窗
|
||||
const open = (data?: TreeNode, parentId?: number) => {
|
||||
if (data) {
|
||||
// 编辑
|
||||
// 编辑:只拷贝需要的字段,避免把 children 等多余字段带到表单
|
||||
isEdit.value = true
|
||||
Object.assign(formData, data)
|
||||
formData.id = data.id
|
||||
formData.parent_id = data.parent_id
|
||||
formData.org_name = data.org_name
|
||||
formData.org_code = data.org_code || ''
|
||||
formData.leader_id = data.leader_id
|
||||
formData.sort = data.sort ?? 0
|
||||
formData.status = data.status ?? 1
|
||||
formData.remark = data.remark || ''
|
||||
} else {
|
||||
// 新增
|
||||
isEdit.value = false
|
||||
@ -148,16 +153,16 @@ const handleSubmit = async () => {
|
||||
try {
|
||||
let res
|
||||
|
||||
// 只允许提交到后台的字段白名单
|
||||
const allowedKeys = ['parent_id', 'org_name', 'org_code', 'leader_id', 'sort', 'status', 'remark']
|
||||
|
||||
if (isEdit.value && formData.id) {
|
||||
// 编辑:使用普通对象提交
|
||||
const submitData: Record<string, any> = {}
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (
|
||||
key !== 'id' &&
|
||||
(formData as any)[key] !== undefined &&
|
||||
(formData as any)[key] !== null
|
||||
) {
|
||||
submitData[key] = (formData as any)[key]
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData[key] = value
|
||||
}
|
||||
})
|
||||
if (tenantId) {
|
||||
@ -167,9 +172,9 @@ const handleSubmit = async () => {
|
||||
} else {
|
||||
// 新增:使用 FormData 以兼容后端 multipart/form-data
|
||||
const submitData = new FormData()
|
||||
Object.keys(formData).forEach(key => {
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (key !== 'id' && value !== undefined && value !== null) {
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData.append(key, String(value))
|
||||
}
|
||||
})
|
||||
|
||||
194
src/views/basicSettings/tenants/components/edit.vue
Normal file
194
src/views/basicSettings/tenants/components/edit.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="formData.id ? '编辑租户' : '添加租户'" width="600px" @closed="handleClosed"
|
||||
destroy-on-close>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px" v-loading="loading"
|
||||
style="padding: 20px">
|
||||
<el-form-item label="租户名称" prop="tenant_name">
|
||||
<el-input v-model="formData.tenant_name" placeholder="请输入租户名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户编码" prop="tenant_code">
|
||||
<el-input v-model="formData.tenant_code" placeholder="系统自动生成" disabled />
|
||||
<div class="form-tip" v-if="!formData.id" style="font-size: 12px; color: #999;">
|
||||
* 编码由系统随机分配,提交时将自动校验唯一性
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contact_person">
|
||||
<el-input v-model="formData.contact_person" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="contact_phone">
|
||||
<el-input v-model="formData.contact_phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电子邮箱" prop="contact_email">
|
||||
<el-input v-model="formData.contact_email" placeholder="请输入电子邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户地址" prop="address">
|
||||
<el-input v-model="formData.address" type="textarea" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { createTenant, editTenant, getTenantDetail, checkTenantCode } from '@/api/tenant';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const initialData = {
|
||||
id: null,
|
||||
tenant_name: '',
|
||||
tenant_code: '',
|
||||
contact_person: '',
|
||||
contact_phone: '',
|
||||
contact_email: '',
|
||||
address: '',
|
||||
status: 1
|
||||
};
|
||||
|
||||
const formData = reactive({ ...initialData });
|
||||
|
||||
const rules = {
|
||||
tenant_name: [{ required: true, message: '请输入租户名称', trigger: 'blur' }],
|
||||
tenant_code: [{ required: true, message: '请输入租户编码', trigger: 'blur' }],
|
||||
contact_phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
const open = (id?: number) => {
|
||||
visible.value = true;
|
||||
Object.assign(formData, initialData);
|
||||
|
||||
if (id) {
|
||||
formData.id = id;
|
||||
fetchDetail(id);
|
||||
} else {
|
||||
formData.tenant_code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDetail = async (id: number) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenantDetail(id);
|
||||
if (res.code === 200) {
|
||||
Object.assign(formData, res.data);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
// 1. 基础表单验证(必填项等)
|
||||
await formRef.value.validate();
|
||||
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
// 2. 如果是新增模式,进入“编码唯一性”校验环
|
||||
if (!formData.id) {
|
||||
let isCodeValid = false;
|
||||
|
||||
while (!isCodeValid) {
|
||||
const res = await checkTenantCode(formData.tenant_code);
|
||||
|
||||
if (res.code === 200) {
|
||||
// 编码可用,跳出循环
|
||||
isCodeValid = true;
|
||||
} else {
|
||||
// 编码重复,重新生成
|
||||
const newCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
|
||||
// 弹出提示框告知用户
|
||||
await ElMessageBox.alert(
|
||||
`租户编码 [${formData.tenant_code}] 已重复,系统已自动为您重新生成为 [${newCode}],请重新点击提交。`,
|
||||
'编码重复提示',
|
||||
{ confirmButtonText: '我知道了', type: 'warning' }
|
||||
);
|
||||
|
||||
formData.tenant_code = newCode;
|
||||
submitting.value = false;
|
||||
return; // 中断本次提交,让用户看一眼新编码后再次点击
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 发起真正的保存请求
|
||||
const saveApi = formData.id ? editTenant(formData.id, formData) : createTenant(formData);
|
||||
const saveRes = await saveApi;
|
||||
|
||||
if (saveRes.code === 200) {
|
||||
ElMessage.success('保存成功');
|
||||
visible.value = false;
|
||||
emit('success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败', error);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成 6 位随机数字并校验唯一性
|
||||
*/
|
||||
const generateUniqueCode = async () => {
|
||||
loading.value = true;
|
||||
let isUnique = false;
|
||||
let newCode = '';
|
||||
let retryCount = 0;
|
||||
const maxRetries = 10; // 保护措施:最多重试10次
|
||||
|
||||
while (!isUnique && retryCount < maxRetries) {
|
||||
// 1. 生成 6 位随机数字字符串
|
||||
newCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
|
||||
try {
|
||||
// 2. 调用接口校验
|
||||
const res = await checkTenantCode(newCode);
|
||||
if (res.code === 200) {
|
||||
isUnique = true; // 接口返回 200 表示不存在,可用
|
||||
} else {
|
||||
console.warn(`编码 ${newCode} 重复,正在重试...`);
|
||||
retryCount++;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("校验编码失败", error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUnique) {
|
||||
formData.tenant_code = newCode;
|
||||
} else {
|
||||
ElMessage.error('无法生成唯一的租户编码,请重试');
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const handleClosed = () => {
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
205
src/views/basicSettings/tenants/index.vue
Normal file
205
src/views/basicSettings/tenants/index.vue
Normal file
@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>租户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="editRef.open()">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加租户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="租户名称">
|
||||
<el-input v-model="searchForm.tenant_name" placeholder="请输入租户名称" clearable
|
||||
@keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户编码">
|
||||
<el-input v-model="searchForm.tenant_code" placeholder="请输入租户编码" clearable
|
||||
@keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="searchForm.contact_person" placeholder="请输入联系人" clearable
|
||||
@keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="searchForm.contact_phone" placeholder="请输入联系电话" clearable
|
||||
@keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 租户列表表格 -->
|
||||
<el-table :data="tenants" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
||||
<el-table-column prop="tenant_name" label="租户名称" min-width="220" align="center" />
|
||||
<el-table-column prop="tenant_code" label="租户编码" min-width="120" align="center" />
|
||||
<el-table-column prop="contact_person" label="联系人" min-width="120" align="center" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" min-width="120" align="center" />
|
||||
<el-table-column prop="contact_email" label="电子邮箱" min-width="180" align="center" />
|
||||
<el-table-column prop="address" label="租户地址" min-width="300" align="center" />
|
||||
<el-table-column prop="status" label="租户状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
||||
scope.row.status === 1 ? "启用" : "禁用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
||||
<el-button size="small" @click="editRef.open(scope.row.id)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<EditModal ref="editRef" @success="refresh" />
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
|
||||
layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getTenantList, deleteTenant } from '@/api/tenant';
|
||||
import { onMounted, ref, reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import EditModal from './components/edit.vue';
|
||||
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const loading = ref(false);
|
||||
const router = useRouter();
|
||||
const tenants = ref([]);
|
||||
|
||||
const editRef = ref();
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除该租户吗?', '提示', {
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteTenant(row.id).then(res => {
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功');
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}).catch(() => { });
|
||||
};
|
||||
|
||||
// 预览
|
||||
const handlePreview = (row) => {
|
||||
router.push(`/basicSettings/tenants/detail/${row.id}`);
|
||||
};
|
||||
|
||||
// 分页改变
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
refresh();
|
||||
};
|
||||
|
||||
// 搜索表单数据
|
||||
const searchForm = reactive({
|
||||
tenant_name: '',
|
||||
tenant_code: '',
|
||||
contact_person: '',
|
||||
contact_phone: ''
|
||||
});
|
||||
|
||||
// 执行查询(回到第一页再刷新)
|
||||
const handleSearch = () => {
|
||||
page.value = 1;
|
||||
refresh();
|
||||
};
|
||||
|
||||
// 重置查询
|
||||
const resetSearch = () => {
|
||||
searchForm.tenant_name = '';
|
||||
searchForm.tenant_code = '';
|
||||
searchForm.contact_person = '';
|
||||
searchForm.contact_phone = '';
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
loading.value = true;
|
||||
|
||||
// 整合分页参数和搜索参数
|
||||
const queryParams = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
...searchForm
|
||||
};
|
||||
|
||||
getTenantList(queryParams).then(res => {
|
||||
if (res.code === 200) {
|
||||
tenants.value = res.data.list;
|
||||
total.value = res.data.total;
|
||||
}
|
||||
}).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 获取租户列表
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
background: #f9f9f9;
|
||||
padding: 20px 20px 0 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user