first commit

This commit is contained in:
2026-06-03 10:09:03 +08:00
commit 81f5039458
6266 changed files with 188236 additions and 0 deletions
@@ -0,0 +1,172 @@
<template>
<el-dialog v-model="visible" title="角色详情" width="600px" @close="handleClose">
<el-descriptions :column="1" border v-loading="loading">
<el-descriptions-item label="角色ID">
{{ roleDetail.id }}
</el-descriptions-item>
<el-descriptions-item label="角色名称">
{{ roleDetail.name }}
</el-descriptions-item>
<el-descriptions-item label="角色分类">
<el-tag :type="Number(roleDetail.cid) === 2 ? 'warning' : 'primary'" size="small">
{{ Number(roleDetail.cid) === 2 ? "租户角色" : "平台角色" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="roleDetail.status === 1 ? 'success' : 'info'" size="small">
{{ roleDetail.status === 1 ? "启用" : "禁用" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="权限列表">
<div v-if="menuNames.length > 0" style="max-height: 300px; overflow-y: auto;">
<el-tag
v-for="(name, index) in menuNames"
:key="index"
style="margin: 4px;"
size="small"
>
{{ name }}
</el-tag>
</div>
<span v-else style="color: var(--el-text-color-secondary);">
暂无权限
</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ roleDetail.create_time }}
</el-descriptions-item>
<el-descriptions-item label="更新时间">
{{ roleDetail.update_time }}
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, computed } from "vue";
import { ElMessage } from "element-plus";
import { getRoleById } from "@/api/role";
import { getAllMenus } from "@/api/menu";
interface Props {
modelValue: boolean;
roleId?: number | null;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
roleId: null,
});
const emit = defineEmits(["update:modelValue"]);
const visible = ref(false);
const loading = ref(false);
const roleDetail = ref<any>({});
const allMenus = ref<any[]>([]);
// 监听 modelValue
watch(
() => props.modelValue,
async (val) => {
visible.value = val;
if (val && props.roleId) {
await loadRoleDetail();
}
}
);
// 监听 visible
watch(visible, (val) => {
emit("update:modelValue", val);
});
// 解析权限ID
const parseRights = (rights: any): number[] => {
if (!rights) return [];
if (Array.isArray(rights)) return rights;
if (typeof rights === "string") {
try {
const parsed = JSON.parse(rights);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
return [];
};
// 递归获取所有菜单ID和名称的映射
const getMenuMap = (menus: any[]): Map<number, string> => {
const map = new Map<number, string>();
const traverse = (items: any[]) => {
for (const item of items) {
map.set(item.id, item.title || item.name || `菜单${item.id}`);
if (item.children && item.children.length > 0) {
traverse(item.children);
}
}
};
traverse(menus);
return map;
};
// 计算菜单名称列表
const menuNames = computed(() => {
if (!roleDetail.value.rights) return [];
const rightIds = parseRights(roleDetail.value.rights);
const menuMap = getMenuMap(allMenus.value);
return rightIds
.map((id) => menuMap.get(id))
.filter((name) => name !== undefined) as string[];
});
// 加载角色详情
const loadRoleDetail = async () => {
if (!props.roleId) return;
loading.value = true;
try {
const res = await getRoleById(props.roleId);
if (res.code === 200) {
roleDetail.value = res.data || {};
await loadMenus(Number(roleDetail.value?.cid || 1));
} else {
ElMessage.error(res.msg || "获取角色详情失败");
}
} catch (error) {
console.error("加载角色详情失败:", error);
ElMessage.error("获取角色详情失败");
} finally {
loading.value = false;
}
};
// 加载菜单树
const loadMenus = async (cid = 1) => {
try {
const res = await getAllMenus({ cid });
if (res.code === 200) {
allMenus.value = res.data || [];
}
} catch (error) {
console.error("加载菜单失败:", error);
}
};
// 关闭对话框
const handleClose = () => {
visible.value = false;
roleDetail.value = {};
};
</script>
<style scoped>
:deep(.el-descriptions__label) {
width: 120px;
}
</style>
@@ -0,0 +1,295 @@
<template>
<el-dialog v-model="visible" :title="isEdit ? '编辑角色' : '添加角色'" width="600px" @close="handleClose">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="角色名称" prop="name">
<el-input v-model="form.name" placeholder="请输入角色名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="角色分类" prop="cid">
<el-radio-group v-model="form.cid" @change="handleRoleTypeChange">
<el-radio :label="1">平台角色</el-radio>
<el-radio :label="2">租户角色</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="权限设置" prop="rights">
<div style="margin-bottom: 10px;">
<el-button size="small" @click="toggleSelectAll">
{{ isAllSelected ? '全不选' : '全选' }}
</el-button>
<el-button size="small" @click="handleExpandAll">展开/折叠</el-button>
</div>
<el-tree ref="treeRef" :data="menuTree" show-checkbox node-key="id"
:props="{ children: 'children', label: 'title' }" @check="updateSelectStatus"
style="width: 100%; border: 1px solid var(--el-border-color); border-radius: 4px; padding: 10px; max-height: 400px; overflow-y: auto;" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { createRole, updateRole } from "@/api/role";
import { getAllMenus } from "@/api/menu";
interface Props {
modelValue: boolean;
role?: any;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
role: null,
});
const emit = defineEmits(["update:modelValue", "success"]);
// --- 状态定义 ---
const visible = ref(false);
const isEdit = ref(false);
const submitting = ref(false);
const formRef = ref();
const treeRef = ref();
const menuTree = ref<any[]>([]);
const isAllSelected = ref(false);
const isExpandAll = ref(false);
const form = ref({
name: "",
cid: 1,
status: 1,
rights: [] as number[],
});
const rules = {
name: [
{ required: true, message: "请输入角色名称", trigger: "blur" },
{ min: 2, max: 50, message: "角色名称长度在 2 到 50 个字符", trigger: "blur" },
],
status: [{ required: true, message: "请选择状态", trigger: "change" }],
cid: [{ required: true, message: "请选择角色分类", trigger: "change" }],
};
// --- 辅助功能 ---
// 获取树中所有节点的 ID 列表
const getAllMenuIds = (data: any[]): number[] => {
const ids: number[] = [];
const traverse = (list: any[]) => {
list.forEach((item) => {
ids.push(item.id);
if (item.children && item.children.length > 0) traverse(item.children);
});
};
traverse(data);
return ids;
};
// 展开折叠
const handleExpandAll = () => {
if (!treeRef.value) return;
// 切换状态
isExpandAll.value = !isExpandAll.value;
// 获取树的所有节点对象
// nodesMap 包含了树中所有的节点实例
const nodes = treeRef.value.store.nodesMap;
for (let id in nodes) {
// 强制修改每个节点的 expanded 属性
nodes[id].expanded = isExpandAll.value;
}
};
// 更新“全选/全不选”按钮的状态
const updateSelectStatus = () => {
if (!treeRef.value || menuTree.value.length === 0) {
isAllSelected.value = false;
return;
}
const checkedKeys = treeRef.value.getCheckedKeys();
const allIds = getAllMenuIds(menuTree.value);
// 只有当勾选数等于总数时,才显示“全不选”
isAllSelected.value = allIds.length > 0 && checkedKeys.length === allIds.length;
};
// 全选/全不选 切换逻辑
const toggleSelectAll = () => {
if (!treeRef.value) return;
if (isAllSelected.value) {
// 当前是全选状态 -> 执行清空
treeRef.value.setCheckedKeys([]);
isAllSelected.value = false;
} else {
// 当前不是全选状态 -> 执行全选
const allIds = getAllMenuIds(menuTree.value);
treeRef.value.setCheckedKeys(allIds);
isAllSelected.value = true;
}
};
// --- 数据加载与监听 ---
// 加载菜单树数据
const loadMenus = async (cid?: number) => {
try {
const res = await getAllMenus({ cid: cid ?? form.value.cid });
if (res.code === 200) {
menuTree.value = res.data || [];
}
} catch (error) {
console.error("加载菜单失败:", error);
ElMessage.error("加载菜单失败");
}
};
// 解析权限数据
const parseRights = (rights: any): number[] => {
if (!rights) return [];
if (Array.isArray(rights)) return rights;
if (typeof rights === "string") {
try {
const parsed = JSON.parse(rights);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
return [];
};
// 核心监听:处理弹窗打开时的初始化
watch(
() => props.modelValue,
async (val) => {
visible.value = val;
if (val) {
// 判断是编辑还是新增
if (props.role) {
isEdit.value = true;
form.value = {
name: props.role.name,
cid: Number(props.role.cid || 1),
status: props.role.status,
rights: parseRights(props.role.rights),
};
await loadMenus(form.value.cid);
// 设置树的回显
nextTick(() => {
if (treeRef.value) {
treeRef.value.setCheckedKeys(form.value.rights);
updateSelectStatus(); // 初始化按钮状态
}
});
} else {
isEdit.value = false;
isAllSelected.value = false;
resetForm();
await loadMenus(1);
}
}
}
);
watch(visible, (val) => {
emit("update:modelValue", val);
});
// --- 表单操作 ---
const resetForm = () => {
form.value = {
name: "",
cid: 1,
status: 1,
rights: [],
};
if (formRef.value) formRef.value.clearValidate();
if (treeRef.value) treeRef.value.setCheckedKeys([]);
};
const handleRoleTypeChange = async () => {
if (!visible.value) return;
if (treeRef.value) {
treeRef.value.setCheckedKeys([]);
}
form.value.rights = [];
isAllSelected.value = false;
await loadMenus(form.value.cid);
};
const handleClose = () => {
visible.value = false;
resetForm();
};
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
// 只获取完全选中的节点(不包含半选父节点)
const checkedKeys = treeRef.value.getCheckedKeys();
const submitData: any = {
name: form.value.name,
cid: form.value.cid,
status: form.value.status,
rights: checkedKeys,
};
// 编辑时添加 id 字段
if (isEdit.value && props.role) {
submitData.id = props.role.id;
}
submitting.value = true;
let res;
if (isEdit.value && props.role) {
res = await updateRole(props.role.id, submitData);
} else {
res = await createRole(submitData);
}
if (res.code === 200) {
ElMessage.success(isEdit.value ? "更新成功" : "创建成功");
emit("success");
handleClose();
} else {
ElMessage.error(res.msg || "操作失败");
}
} catch (error: any) {
if (error !== false) {
ElMessage.error(error.message || "操作失败");
}
} finally {
submitting.value = false;
}
};
</script>
<style scoped>
:deep(.el-tree) {
background-color: var(--el-bg-color);
}
</style>
@@ -0,0 +1,231 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>角色管理</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
添加角色
</el-button>
<el-button @click="refresh">
<el-icon><Refresh /></el-icon>
刷新</el-button
>
</div>
</div>
<el-divider></el-divider>
<!-- 错误状态 -->
<div v-if="error" class="error-state">
<el-alert title="加载失败" :message="error" type="error" show-icon />
<el-button type="primary" @click="refresh">重试</el-button>
</div>
<!-- 角色列表 -->
<div v-else>
<el-table :data="roles" stripe style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column
prop="name"
label="角色名称"
min-width="150"
align="center"
/>
<el-table-column prop="cid" label="角色分类" width="120" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.cid) === 2 ? 'warning' : 'primary'" size="small">
{{ Number(row.cid) === 2 ? "租户角色" : "平台角色" }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="rights"
label="权限"
min-width="150"
align="center"
/>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button
size="small"
type="primary"
link
@click="handleView(row)"
>
<el-icon><View /></el-icon>
详情
</el-button>
<el-button
size="small"
type="primary"
link
@click="handleEdit(row)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button
size="small"
type="danger"
link
@click="handleDelete(row)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 编辑对话框 -->
<RoleEditDialog
v-model="editDialogVisible"
:role="currentRole"
@success="handleEditSuccess"
/>
<!-- 详情对话框 -->
<RoleDetailDialog
v-model="detailDialogVisible"
:roleId="currentRoleId"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, View, Edit, Delete, Refresh } from "@element-plus/icons-vue";
import { getAllRoles, getRoleById, deleteRole } from "@/api/role";
import RoleEditDialog from "./components/edit.vue";
import RoleDetailDialog from "./components/detail.vue";
import { useAuthStore } from "@/stores/auth";
// 使用 auth store 获取用户信息
const authStore = useAuthStore();
const roles = ref<any[]>([]);
const loading = ref(false);
const error = ref("");
const editDialogVisible = ref(false);
const detailDialogVisible = ref(false);
const currentRole = ref<any>(null);
const currentRoleId = ref<number | null>(null);
// 获取用户信息
const userInfo = authStore.user;
if (userInfo && userInfo.id) {
// console.log('用户名:', userInfo.username || userInfo.nickname);
// console.log('用户ID:', userInfo.id);
// console.log('角色:', userInfo.role);
} else {
// console.log('未找到用户信息或用户未登录');
}
// 获取角色列表
const fetchRoles = async () => {
loading.value = true;
error.value = "";
try {
const res = await getAllRoles();
if (res.code === 200) {
roles.value = res.data || [];
// console.log('角色列表:', roles.value);
} else {
error.value = res.msg || "获取角色列表失败";
ElMessage.error(error.value);
}
} catch (err: any) {
error.value = err.message || "获取角色列表失败";
ElMessage.error(error.value);
} finally {
loading.value = false;
}
};
// 刷新界面
async function refresh() {
await fetchRoles();
if (!error.value) {
ElMessage.success("刷新成功");
}
}
// 添加角色
function handleAdd() {
currentRole.value = null;
editDialogVisible.value = true;
}
// 查看详情
function handleView(row: any) {
currentRoleId.value = row.id;
detailDialogVisible.value = true;
}
// 编辑角色
function handleEdit(row: any) {
currentRole.value = { ...row };
editDialogVisible.value = true;
}
// 删除角色
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(
`确定要删除角色「${row.name}」吗?删除后不可恢复。`,
"警告",
{ type: "warning" }
);
loading.value = true;
try {
const res = await deleteRole(row.id);
if (res.code === 200) {
ElMessage.success("删除成功");
fetchRoles();
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (err: any) {
ElMessage.error(err.message || "删除失败");
} finally {
loading.value = false;
}
} catch {
// 取消删除
}
}
// 编辑成功回调
function handleEditSuccess() {
fetchRoles();
}
onMounted(() => {
fetchRoles();
});
</script>
<style scoped>
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 220px;
padding: 32px 0 16px 0;
background: var(--el-bg-color-page);
border-radius: 5px;
gap: 16px;
}
</style>
@@ -0,0 +1,92 @@
<template>
<el-form ref="contactFormRef" :model="contactForm" :rules="contactRules" label-width="120px" style="max-width: 600px">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="contactForm.phone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="联系邮箱" prop="email">
<el-input v-model="contactForm.email" placeholder="请输入联系邮箱" />
</el-form-item>
<el-form-item label="公司地址" prop="address">
<el-input v-model="contactForm.address" placeholder="请输入公司地址" />
</el-form-item>
<el-form-item label="工作时间" prop="workTime">
<el-input v-model="contactForm.workTime" placeholder="如:周一至周五 9:00-18:00" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveContactSettings">保存设置</el-button>
<el-button @click="resetContactForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
import { getCompanyInfos, saveCompanyInfos } from "@/api/sitesettings";
import { useAuthStore } from "@/stores/auth";
const authStore = useAuthStore();
const contactFormRef = ref<FormInstance>();
const contactForm = reactive({
phone: "",
email: "",
address: "",
workTime: ""
});
const contactRules: FormRules = {
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }]
};
// 获取企业信息
const initCompanyInfos = async () => {
const res = await getCompanyInfos(authStore.user.tid);
if (res.code === 200 && res.data) {
const data = res.data;
contactForm.phone = data.contact_phone || "";
contactForm.email = data.contact_email || "";
contactForm.address = data.address || "";
contactForm.workTime = data.worktime || "";
}
};
const saveContactSettings = async () => {
if (!contactFormRef.value) return;
await contactFormRef.value.validate(async (valid) => {
if (valid) {
const data = {
tid: authStore.user.tid,
contact_phone: contactForm.phone,
contact_email: contactForm.email,
address: contactForm.address,
worktime: contactForm.workTime
};
const res = await saveCompanyInfos(data);
if (res.code === 200) {
ElMessage.success("联系方式保存成功");
} else {
ElMessage.error(res.msg || "保存失败");
}
}
});
};
const resetContactForm = () => {
contactForm.phone = "";
contactForm.email = "";
contactForm.address = "";
contactForm.workTime = "";
};
defineExpose({
contactForm
});
// 初始化数据
onMounted(() => {
initCompanyInfos();
});
</script>
@@ -0,0 +1,102 @@
<template>
<el-form
ref="legalNoticeFormRef"
:model="formData"
label-width="120px"
style="max-width: 600px"
>
<el-form-item label="法律声明" prop="legalNotice">
<el-input
v-model="legalNotice"
type="textarea"
:rows="8"
placeholder="请输入法律声明"
/>
</el-form-item>
<el-form-item label="隐私条款" prop="privacyTerms">
<el-input
v-model="privacyTerms"
type="textarea"
:rows="8"
placeholder="请输入隐私条款"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSaveLegalInfos">保存设置</el-button>
<el-button @click="resetLegalNoticeForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
import { getLegalInfos, saveLegalInfos } from "@/api/sitesettings";
import { useAuthStore } from "@/stores/auth";
const authStore = useAuthStore();
const legalNoticeFormRef = ref<FormInstance>();
const legalNotice = ref("");
const privacyTerms = ref("");
const seoForm = reactive({
legalNotice: "",
privacyTerms: "",
});
const formData = {
legalNotice,
privacyTerms,
};
//调用法律声明和隐私条款数据
const initLegalInfos = async () => {
const res = await getLegalInfos();
if (res.code === 200 && res.data) {
const data = res.data;
const dataMap: Record<string, string> = {};
data.forEach((item: any) => {
dataMap[item.label] = item.value;
});
legalNotice.value = dataMap["legalNotice"] || "";
privacyTerms.value = dataMap["privacyTerms"] || "";
}
};
const handleSaveLegalInfos = async () => {
if (!legalNoticeFormRef.value) return;
await legalNoticeFormRef.value.validate(async (valid) => {
if (valid) {
const data = {
tid: authStore.user.tid,
legalNotice: legalNotice.value,
privacyTerms: privacyTerms.value
};
const res = await saveLegalInfos(data);
if (res.code === 200) {
ElMessage.success("保存成功");
} else {
ElMessage.error(res.msg || "保存失败");
}
}
});
};
const resetLegalNoticeForm = () => {
legalNotice.value = "";
privacyTerms.value = "";
};
defineExpose({
legalNotice,
privacyTerms,
});
// 初始化法律声明和隐私条款数据
onMounted(() => {
initLegalInfos();
});
</script>
@@ -0,0 +1,81 @@
<template>
<el-form ref="formRef" :model="form" label-width="120px">
<el-form-item label="开启验证" prop="openVerify">
<el-switch v-model="form.openVerify" />
</el-form-item>
<el-form-item label="验证模式" prop="verifyModel">
<el-radio-group v-model="form.verifyModel" :disabled="!form.openVerify">
<el-radio-button value="1">图形</el-radio-button>
<el-radio-button value="2">短信</el-radio-button>
<el-radio-button value="3">邮箱</el-radio-button>
<el-radio-button value="4">极验3.0</el-radio-button>
<el-radio-button value="5">极验4.0</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="form.verifyModel === '4'">
<el-form-item label="极验ID" prop="geetest3ID">
<el-input v-model="form.geetest3ID" />
</el-form-item>
<el-form-item label="极验KEY" prop="geetest3KEY">
<el-input v-model="form.geetest3KEY" />
</el-form-item>
</template>
<template v-if="form.verifyModel === '5'">
<el-form-item label="极验ID" prop="geetest4ID">
<el-input v-model="form.geetest4ID" />
</el-form-item>
<el-form-item label="极验KEY" prop="geetest4KEY">
<el-input v-model="form.geetest4KEY" />
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="formRef?.resetFields()">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { getVerifyInfos, saveVerifyInfos } from "@/api/sitesettings";
const formRef = ref();
const form = reactive({
openVerify: false,
verifyModel: "1",
geetest3ID: "",
geetest3KEY: "",
geetest4ID: "",
geetest4KEY: ""
});
const init = async () => {
const { code, data } = await getVerifyInfos();
if (code === 200 && Array.isArray(data)) {
const formData = {};
data.forEach(item => {
if (item.label === 'openVerify') {
formData[item.label] = item.value === '1';
} else {
formData[item.label] = item.value;
}
});
Object.assign(form, formData);
}
};
const handleSave = async () => {
const { code } = await saveVerifyInfos(form);
if (code === 200) ElMessage.success("保存成功");
};
init();
defineExpose({ form, init });
</script>
@@ -0,0 +1,289 @@
<template>
<el-form
ref="normalFormRef"
:model="formData"
:rules="normalRules"
label-width="120px"
style="max-width: 600px"
>
<el-form-item label="站点名称" prop="sitename">
<el-input v-model="sitename" placeholder="请输入站点名称" />
</el-form-item>
<el-form-item label="站点Logo" prop="logo">
<el-upload
class="logo-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="handleLogoChange"
>
<img
v-if="logo"
:src="API_BASE_URL + logo.replace(/^\//, '/')"
class="logo-image"
/>
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="站点Logo-浅色" prop="logow">
<el-upload
class="logo-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="handleLogowChange"
>
<img
v-if="logow"
:src="API_BASE_URL + logow.replace(/^\//, '/')"
class="logo-image darkbg"
/>
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="站点图标" prop="ico">
<el-upload
class="logo-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="handleIconChange"
>
<img
v-if="ico"
:src="API_BASE_URL + ico.replace(/^\//, '/')"
class="logo-image"
/>
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="form-tip">请上传ico格式文件</div>
</el-form-item>
<el-form-item label="企业名称" prop="companyname">
<el-input v-model="companyname" placeholder="请输入企业名称" />
</el-form-item>
<el-form-item label="企业介绍" prop="companyintroduction">
<el-input v-model="companyintroduction" type="textarea" rows="5" placeholder="请输入企业介绍" />
</el-form-item>
<el-form-item label="站点简介" prop="description">
<el-input v-model="description" placeholder="请输入站点简介" />
</el-form-item>
<el-form-item label="版权信息" prop="copyright">
<el-input v-model="copyright" placeholder="如:© 2026 公司名称" />
</el-form-item>
<el-form-item label="备案号 " prop="icp">
<el-input v-model="icp" placeholder="如:苏ICP备20260000号" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSaveNormalInfos"
>保存设置</el-button
>
<el-button @click="resetnormalForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import type { FormInstance, FormRules, UploadFile } from "element-plus";
import { getNormalInfos, saveNormalInfos } from "@/api/sitesettings";
import { uploadFile } from "@/api/file";
import { useAuthStore } from "@/stores/auth";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const authStore = useAuthStore();
const normalFormRef = ref<FormInstance>();
const sitename = ref("");
const companyname = ref("");
const companyintroduction = ref("");
const logo = ref("");
const logow = ref("");
const description = ref("");
const copyright = ref("");
const icp = ref("");
const ico = ref("");
const formData = {
sitename,
companyname,
companyintroduction,
logo,
logow,
ico,
description,
copyright,
icp,
};
const normalRules: FormRules = {
sitename: [{ required: true, message: "请输入站点名称", trigger: "blur" }],
};
//调用基础数据
const initNormalInfos = async () => {
const res = await getNormalInfos(authStore.user.tid);
if (res.code === 200 && res.data) {
const data = res.data;
sitename.value = data.sitename || "";
companyintroduction.value = data.companyintroduction || "";
logo.value = data.logo || "";
logow.value = data.logow || "";
ico.value = data.ico || "";
description.value = data.description || "";
copyright.value = data.copyright || "";
icp.value = data.icp || "";
companyname.value = data.companyname || "";
}
};
const handleLogoChange = (file: UploadFile) => {
const uploadFormData = new FormData();
uploadFormData.append("file", file.raw);
uploadFormData.append("cate", "site");
uploadFile(uploadFormData).then((uploadRes) => {
if (
(uploadRes.code === 200 || uploadRes.code === 201) &&
uploadRes.data &&
uploadRes.data.url
) {
logo.value = uploadRes.data.url.replace(/\\/g, "/");
} else {
ElMessage.error(uploadRes.msg || "上传失败");
}
});
};
const handleLogowChange = (file: UploadFile) => {
const uploadFormData = new FormData();
uploadFormData.append("file", file.raw);
uploadFormData.append("cate", "site");
uploadFile(uploadFormData).then((uploadRes) => {
if (
(uploadRes.code === 200 || uploadRes.code === 201) &&
uploadRes.data &&
uploadRes.data.url
) {
logow.value = uploadRes.data.url.replace(/\\/g, "/");
} else {
ElMessage.error(uploadRes.msg || "上传失败");
}
});
};
const handleSaveNormalInfos = async () => {
if (!normalFormRef.value) return;
await normalFormRef.value.validate(async (valid) => {
if (valid) {
const data = {
tid: authStore.user.tid,
sitename: sitename.value,
companyintroduction: companyintroduction.value,
logo: logo.value,
logow: logow.value,
ico: ico.value,
description: description.value,
companyname: companyname.value,
copyright: copyright.value,
icp: icp.value,
};
const res = await saveNormalInfos(data);
if (res.code === 200) {
ElMessage.success("保存成功");
} else {
ElMessage.error(res.msg || "保存失败");
}
}
});
};
const handleIconChange = (file: UploadFile) => {
const uploadFormData = new FormData();
uploadFormData.append("file", file.raw);
uploadFormData.append("cate", "site");
uploadFile(uploadFormData).then((uploadRes) => {
if ((uploadRes.code === 200 || uploadRes.code === 201) && uploadRes.data?.url) {
ico.value = uploadRes.data.url.replace(/\\/g, "/");
} else {
ElMessage.error(uploadRes.msg || "上传失败");
}
});
};
const resetnormalForm = () => {
sitename.value = "";
companyname.value = "";
companyintroduction.value = "";
logo.value = "";
logow.value = "";
ico.value = "";
description.value = "";
copyright.value = "";
icp.value = "";
};
defineExpose({
sitename,
companyname,
companyintroduction,
logo,
logow,
ico,
description,
copyright,
icp,
});
// 初始化基础数据
onMounted(() => {
initNormalInfos();
});
</script>
<style lang="less" scoped>
.darkbg {
background-color: #efefef;
}
.form-tip{
margin-left: 20px;
color: #999;
font-size: 12px;
margin-top: 5px;
}
.logo-uploader {
:deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
&:hover {
border-color: #3973ff;
}
}
.logo-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.logo-image {
width: 100px;
height: 100px;
object-fit: contain;
display: block;
}
}
</style>
@@ -0,0 +1,58 @@
<template>
<el-form
ref="otherFormRef"
:model="otherForm"
label-width="120px"
style="max-width: 600px"
>
<el-form-item label="开启维护模式">
<el-switch v-model="otherForm.maintenanceMode" />
<span class="form-tip">开启后前台将显示维护中页面</span>
</el-form-item>
<el-form-item label="开启注册">
<el-switch v-model="otherForm.allowRegister" />
<span class="form-tip">允许用户在前台注册账号</span>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveOtherSettings">保存设置</el-button>
<el-button @click="resetOtherForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import type { FormInstance } from "element-plus";
const otherFormRef = ref<FormInstance>();
const otherForm = reactive({
maintenanceMode: false,
allowRegister: false,
captchaEnabled: true
});
const saveOtherSettings = () => {
// TODO: 保存其他设置
ElMessage.success("其他设置保存成功");
};
const resetOtherForm = () => {
otherForm.maintenanceMode = false;
otherForm.allowRegister = false;
otherForm.captchaEnabled = true;
};
defineExpose({
otherForm
});
</script>
<style lang="less" scoped>
.form-tip {
margin-left: 10px;
font-size: 12px;
color: #909399;
}
</style>
@@ -0,0 +1,101 @@
<template>
<el-form
ref="seoFormRef"
:model="seoForm"
:rules="seoRules"
label-width="120px"
style="max-width: 600px"
>
<el-form-item label="SEO标题" prop="seoTitle">
<el-input v-model="seoForm.seoTitle" placeholder="请输入SEO标题" />
</el-form-item>
<el-form-item label="SEO关键词" prop="seoKeywords">
<el-input
v-model="seoForm.seoKeywords"
placeholder="多个关键词用逗号分隔"
/>
</el-form-item>
<el-form-item label="SEO描述" prop="seoDescription">
<el-input
v-model="seoForm.seoDescription"
type="textarea"
:rows="3"
placeholder="请输入SEO描述"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveSeoSettings">保存设置</el-button>
<el-button @click="resetSeoForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElLoading } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
import { getCompanySeo, saveCompanySeo } from "@/api/sitesettings";
const seoFormRef = ref<FormInstance>();
const seoForm = reactive({
seoTitle: "",
seoKeywords: "",
seoDescription: "",
});
const seoRules: FormRules = {
seoTitle: [{ required: true, message: "请输入SEO标题", trigger: "blur" }],
};
const fetchSeoData = async () => {
try {
const res = await getCompanySeo();
if (res.code === 200) {
Object.assign(seoForm, res.data);
}
} catch (error) {
ElMessage.error("获取SEO数据失败");
}
};
const saveSeoSettings = async () => {
if (!seoFormRef.value) return;
await seoFormRef.value.validate(async (valid) => {
if (valid) {
const loading = ElLoading.service({ text: "正在保存..." });
try {
const res = await saveCompanySeo({
seoTitle: seoForm.seoTitle,
seoKeywords: seoForm.seoKeywords,
seoDescription: seoForm.seoDescription,
});
if (res.code === 200) {
ElMessage.success("SEO设置保存成功");
} else {
ElMessage.error(res.msg);
}
} catch (error) {
ElMessage.error("网络请求失败");
} finally {
loading.close();
}
}
});
};
const resetSeoForm = () => {
if (!seoFormRef.value) return;
seoFormRef.value.resetFields();
};
onMounted(() => {
fetchSeoData();
});
defineExpose({
seoForm,
});
</script>
@@ -0,0 +1,98 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>站点设置</h2>
</div>
<el-divider></el-divider>
<div class="settings-container">
<el-tabs v-model="activeTab" class="settings-tabs">
<el-tab-pane label="基本信息" name="basic">
<normalSettings
ref="normalSettingsRef"
v-if="activeTab === 'basic'"
/>
</el-tab-pane>
<el-tab-pane label="SEO设置" name="seo">
<seoSettings ref="seoSettingsRef" v-if="activeTab === 'seo'" />
</el-tab-pane>
<el-tab-pane label="联系方式" name="contact">
<contactSettings
ref="contactSettingsRef"
v-if="activeTab === 'contact'"
/>
</el-tab-pane>
<el-tab-pane label="登录验证" v-if="roleId === 1" name="loginVerification">
<loginVerificationSettings
ref="loginVerificationSettingsRef"
v-if="activeTab === 'loginVerification'"
/>
</el-tab-pane>
<el-tab-pane label="法律声明&隐私条款" name="legalNotice">
<legalNoticeSettings
ref="legalNoticeSettingsRef"
v-if="activeTab === 'legalNotice'"
/>
</el-tab-pane>
<el-tab-pane label="其他设置" name="other">
<otherSettings ref="otherSettingsRef" v-if="activeTab === 'other'" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import normalSettings from "./components/normalSettings.vue";
import seoSettings from "./components/seoSettings.vue";
import contactSettings from "./components/contactSettings.vue";
import otherSettings from "./components/otherSettings.vue";
import legalNoticeSettings from "./components/legalNotice.vue";
import loginVerificationSettings from "./components/loginVerification.vue";
import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore();
const roleId = computed(() => Number(authStore.user?.rid) || 0);
const activeTab = ref("basic");
</script>
<style lang="less" scoped>
.container-box {
padding: 20px;
border-radius: 4px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--el-text-color-primary);
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
}
.settings-container {
margin-top: 20px;
}
.settings-tabs {
:deep(.el-tabs__header) {
margin-bottom: 20px;
}
:deep(.el-tabs__nav-wrap::after) {
height: 1px;
}
}
</style>
@@ -0,0 +1,244 @@
<template>
<div class="tenant-users-tab">
<div class="section-header">
<div class="section-title">用户列表</div>
<el-button type="primary" size="small" :disabled="!tid" @click="handleAddUser">
<el-icon><Plus /></el-icon>
添加用户
</el-button>
</div>
<el-form :inline="true" class="user-search-form" @submit.prevent>
<el-form-item label="关键词">
<el-input
v-model="userSearchKeyword"
clearable
placeholder="姓名 / 手机 / 邮箱 / 账号"
style="width: 260px"
:disabled="!tid"
@keyup.enter="handleSearchUsers"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :disabled="!tid" @click="handleSearchUsers">查询</el-button>
<el-button :disabled="!tid" @click="resetUserSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="tenantUsers" v-loading="usersLoading" border style="width: 100%">
<el-table-column prop="account" label="用户名" min-width="140" align="center" />
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
{{ Number(row.status) === 1 ? "启用" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" align="center" fixed="right">
<template #default="{ row }">
<el-button text type="primary" @click="openPasswordDialog(row)">
修改密码
</el-button>
</template>
</el-table-column>
</el-table>
<AddUser ref="addUserRef" @success="refreshTenantUsers" />
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="420px" destroy-on-close>
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="90px">
<el-form-item label="新密码" prop="password">
<el-input v-model="passwordForm.password" type="password" show-password placeholder="请输入新密码" />
</el-form-item>
<el-form-item label="确认密码" prop="password2">
<el-input v-model="passwordForm.password2" type="password" show-password placeholder="请再次输入新密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="passwordDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="passwordSubmitting" @click="submitPassword">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue";
import { ElMessage } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { getTenantUserList, editTenantUser } from "@/api/tenantUser";
import AddUser from "./adduser.vue";
const props = defineProps<{
/** 当前租户 ID,为空时不请求 */
tid: number | null;
}>();
const tenantUsers = ref<any[]>([]);
const usersLoading = ref(false);
const userSearchKeyword = ref("");
const addUserRef = ref<{ open: (tenantId: number) => void } | null>(null);
const passwordDialogVisible = ref(false);
const passwordSubmitting = ref(false);
const currentTenantUserId = ref<number | null>(null);
const passwordFormRef = ref();
const passwordForm = reactive({
password: "",
password2: "",
});
const passwordRules = {
password: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 5, message: "密码至少 5 位", trigger: "blur" },
],
password2: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{
validator: (_rule: any, value: string, callback: any) => {
if (!value) return callback(new Error("请再次输入新密码"));
if (value !== passwordForm.password) return callback(new Error("两次密码不一致"));
callback();
},
trigger: "blur",
},
],
};
const buildTenantUserQuery = (tid: number) => {
const params: Record<string, string | number> = { tid };
const kw = userSearchKeyword.value.trim();
if (kw) params.keyword = kw;
return params;
};
const refreshTenantUsers = async () => {
const id = props.tid;
if (id == null) return;
usersLoading.value = true;
try {
const usersRes = await getTenantUserList(buildTenantUserQuery(id));
if (usersRes?.code === 200) {
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
} else {
tenantUsers.value = [];
}
} catch {
tenantUsers.value = [];
} finally {
usersLoading.value = false;
}
};
const loadUsersForTid = async (tid: number) => {
usersLoading.value = true;
try {
const usersRes = await getTenantUserList({ tid });
if (usersRes?.code === 200) {
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
} else {
tenantUsers.value = [];
}
} catch {
tenantUsers.value = [];
} finally {
usersLoading.value = false;
}
};
watch(
() => props.tid,
(id) => {
userSearchKeyword.value = "";
if (id == null) {
tenantUsers.value = [];
return;
}
loadUsersForTid(id);
},
{ immediate: true }
);
const handleSearchUsers = () => {
refreshTenantUsers();
};
const resetUserSearch = () => {
userSearchKeyword.value = "";
refreshTenantUsers();
};
const handleAddUser = () => {
if (props.tid != null) {
addUserRef.value?.open(props.tid);
}
};
const openPasswordDialog = (row: any) => {
currentTenantUserId.value = Number(row?.id || 0) || null;
passwordForm.password = "";
passwordForm.password2 = "";
passwordDialogVisible.value = true;
};
const submitPassword = async () => {
if (!passwordFormRef.value || !currentTenantUserId.value) return;
try {
await passwordFormRef.value.validate();
} catch {
return;
}
passwordSubmitting.value = true;
try {
const res = await editTenantUser(currentTenantUserId.value, { password: passwordForm.password });
if (res.code === 200) {
ElMessage.success("密码修改成功");
passwordDialogVisible.value = false;
} else {
ElMessage.error(res.msg || "密码修改失败");
}
} finally {
passwordSubmitting.value = false;
}
};
defineExpose({
refreshTenantUsers,
});
</script>
<style scoped>
.tenant-users-tab {
min-width: 0;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
.user-search-form {
margin-bottom: 8px;
padding: 8px 12px;
background: var(--el-fill-color-light);
border-radius: 4px;
}
.user-search-form :deep(.el-form-item) {
margin-bottom: 8px;
margin-top: 0;
}
</style>
@@ -0,0 +1,144 @@
<template>
<el-dialog v-model="visible" title="添加用户" 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="account">
<el-input v-model="formData.account" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>
<el-form-item label="确认密码" prop="password2">
<el-input v-model="formData.password2" type="password" placeholder="请再次输入密码" show-password />
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" 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">
/**
* 租户详情内「添加用户」:提交到 /platform/tenantUser/create
* 后端写入 yz_tenant_user(租户-用户绑定,含冗余账号/密码等字段)。
*/
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { createTenantUser } from '@/api/tenantUser';
const emit = defineEmits(['success']);
const visible = ref(false);
const loading = ref(false);
const submitting = ref(false);
const formRef = ref();
const currentTenantId = ref<number | null>(null);
const formData = reactive({
account: '',
password: '',
password2: '',
name: '',
phone: '',
email: '',
status: 1
});
const rules = {
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 5 }],
password2: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (_rule: any, value: string, callback: any) => {
if (!value) {
callback(new Error('请再次输入密码'));
return;
}
if (value !== formData.password) {
callback(new Error('两次输入的密码不一致'));
return;
}
callback();
},
trigger: 'blur'
}
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
]
};
const open = (tenantId: number) => {
visible.value = true;
currentTenantId.value = tenantId;
Object.assign(formData, {
account: '',
password: '',
password2: '',
name: '',
phone: '',
email: '',
status: 1
});
};
const submitForm = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
} catch {
// 校验失败时 validate 会 reject,避免 Uncaught (in promise)
return;
}
submitting.value = true;
try {
const submitData = {
...formData,
tid: currentTenantId.value,
};
// 提交时不需要把确认密码发给后端
delete (submitData as any).password2;
const res = await createTenantUser(submitData);
if (res.code === 200) {
ElMessage.success('添加成功');
visible.value = false;
emit('success');
}
} catch (error) {
console.error('提交失败', error);
} finally {
submitting.value = false;
}
};
const handleClosed = () => {
formRef.value?.resetFields();
};
defineExpose({ open });
</script>
@@ -0,0 +1,160 @@
<template>
<el-drawer
v-model="visible"
title="租户详细信息"
size="1200px"
@closed="handleClosed"
>
<div v-loading="loading" class="detail-container">
<el-descriptions
:column="2"
:label-width="110"
border
class="tenant-detail-descriptions"
>
<el-descriptions-item label="租户名称">
<span class="detail-value">{{ detailData.tenant_name }}</span>
</el-descriptions-item>
<el-descriptions-item label="租户编码">
<el-tag type="info" effect="plain">{{ detailData.tenant_code }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="联系人">
{{ detailData.contact_person || '-' }}
</el-descriptions-item>
<el-descriptions-item label="联系电话">
{{ detailData.contact_phone || '-' }}
</el-descriptions-item>
<el-descriptions-item label="电子邮箱">
{{ detailData.contact_email || '-' }}
</el-descriptions-item>
<el-descriptions-item label="租户地址" :span="2">
{{ detailData.address || '-' }}
</el-descriptions-item>
<el-descriptions-item label="租户状态">
<el-tag :type="detailData.status === 1 ? 'success' : 'danger'">
{{ detailData.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ detailData.create_time || '-' }}
</el-descriptions-item>
</el-descriptions>
<el-divider />
<div class="tenant-detail-body">
<el-tabs
v-model="activeTab"
tab-position="left"
class="tenant-detail-tabs"
>
<el-tab-pane label="租户用户" name="users">
<div class="tab-pane-inner">
<TenantUsersTab :tid="detailTenantId" />
</div>
</el-tab-pane>
<!-- 后续功能在此继续增加 <el-tab-pane label="..." name="...">...</el-tab-pane> -->
</el-tabs>
</div>
<!-- <div class="footer-actions">
<el-button @click="visible = false">关闭</el-button>
</div> -->
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { getTenantDetail } from "@/api/tenant";
import TenantUsersTab from "./TenantUsersTab.vue";
const visible = ref(false);
const loading = ref(false);
const detailData = ref<any>({});
const detailTenantId = ref<number | null>(null);
const activeTab = ref("users");
const open = async (id: number) => {
detailTenantId.value = id;
activeTab.value = "users";
visible.value = true;
loading.value = true;
try {
const detailRes = await getTenantDetail(id);
if (detailRes.code === 200) {
detailData.value = detailRes.data;
}
} catch (error) {
console.error("获取详情失败", error);
} finally {
loading.value = false;
}
};
const handleClosed = () => {
detailData.value = {};
detailTenantId.value = null;
activeTab.value = "users";
};
defineExpose({ open });
</script>
<style scoped>
.detail-container {
padding: 10px 20px;
}
.detail-value {
font-weight: bold;
color: #333;
}
.footer-actions {
margin-top: 24px;
display: flex;
justify-content: flex-end;
}
.tenant-detail-body {
margin-top: 20px;
min-height: 0;
}
.tenant-detail-tabs {
width: 100%;
}
.tenant-detail-tabs :deep(.el-tabs__header) {
margin-right: 0;
}
.tenant-detail-tabs :deep(.el-tabs__nav-wrap.is-left) {
min-width: 112px;
}
.tenant-detail-tabs :deep(.el-tabs__item) {
justify-content: flex-start;
padding: 0 16px;
height: 44px;
line-height: 44px;
}
.tenant-detail-tabs :deep(.el-tabs__content) {
padding: 0 0 0 16px;
min-height: 320px;
}
.tab-pane-inner {
min-width: 0;
}
:deep(.el-descriptions__label) {
width: 120px;
background-color: #f5f7fa;
}
.tenant-detail-descriptions :deep(.el-descriptions__table) {
width: 100%;
table-layout: fixed;
}
</style>
@@ -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_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="tenant_name">
<el-input v-model="formData.tenant_name" placeholder="请输入租户名称" />
</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>
@@ -0,0 +1,166 @@
<template>
<el-dialog v-model="visible" title="资质文件管理" width="650px" destroy-on-close>
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="110px"
v-loading="loading"
style="padding: 10px 20px"
>
<el-form-item label="租户名称">
<el-input v-model="tenantName" disabled />
</el-form-item>
<el-form-item label="资质类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择资质类型" style="width: 100%">
<el-option label="营业执照" value="business_license" />
<el-option label="开户许可证" value="bank_account_permit" />
<el-option label="行业许可证" value="industry_license" />
<el-option label="其他资质" value="others" />
</el-select>
</el-form-item>
<el-form-item label="资质图片" prop="file_url">
<el-upload
class="avatar-uploader"
action="/api/platform/common/upload"
:show-file-list="false"
:on-success="handleUploadSuccess"
:before-upload="beforeAvatarUpload"
name="file"
>
<img v-if="formData.file_url" :src="formData.file_url" class="qualification-img" />
<el-icon v-else class="uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="upload-tip">支持 jpg/png 格式大小不超过 2MB</div>
</el-form-item>
<el-form-item label="有效期至" prop="expire_time">
<el-date-picker
v-model="formData.expire_time"
type="date"
placeholder="选择过期日期"
style="width: 100%"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="备注说明">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入备注信息" />
</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 } from 'element-plus';
import { Plus } from '@element-plus/icons-vue';
// 假设你有对应的资质接口
// import { saveQualification, getQualificationDetail } from '@/api/tenant';
const visible = ref(false);
const loading = ref(false);
const submitting = ref(false);
const tenantName = ref('');
const formRef = ref();
const formData = reactive({
tid: null,
type: '',
file_url: '',
expire_time: '',
remark: ''
});
const rules = {
type: [{ required: true, message: '请选择资质类型', trigger: 'change' }],
file_url: [{ required: true, message: '请上传资质图片', trigger: 'change' }],
expire_time: [{ required: true, message: '请选择有效期', trigger: 'change' }]
};
// 打开弹窗
const open = (row: any) => {
visible.value = true;
tenantName.value = row.tenant_name;
formData.tid = row.id;
// 如果后端有资质详情接口,可以在此获取回显数据
// fetchDetail(row.id);
};
// 上传前的校验
const beforeAvatarUpload = (rawFile: any) => {
if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') {
ElMessage.error('图片必须是 JPG 或 PNG 格式!');
return false;
} else if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('图片大小不能超过 2MB!');
return false;
}
return true;
};
// 上传成功回调
const handleUploadSuccess = (response: any) => {
// 根据你后端的上传接口返回结构调整
formData.file_url = response.data.url;
ElMessage.success('上传成功');
};
const submitForm = async () => {
await formRef.value.validate();
submitting.value = true;
try {
// 模拟提交
console.log('提交的数据:', formData);
ElMessage.success('资质保存成功');
visible.value = false;
} finally {
submitting.value = false;
}
};
defineExpose({ open });
</script>
<style scoped>
.avatar-uploader {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
width: 178px;
height: 178px;
transition: border-color 0.3s;
}
.avatar-uploader:hover {
border-color: #409eff;
}
.uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
line-height: 178px !important;
}
.qualification-img {
width: 178px;
height: 178px;
display: block;
object-fit: contain;
}
.upload-tip {
font-size: 12px;
color: #999;
margin-top: 8px;
}
</style>
@@ -0,0 +1,258 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>我的域名</h2>
<el-button type="primary" @click="dialogVisible = true">
<el-icon><Plus /></el-icon>
申请二级域名
</el-button>
</div>
<el-divider></el-divider>
<!-- 我的域名列表 -->
<el-table
:data="tableData"
style="width: 100%"
border
v-loading="loading"
element-loading-text="正在加载..."
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="sub_domain" label="二级域名前缀" width="150" />
<el-table-column prop="main_domain" label="主域名" min-width="150" />
<el-table-column prop="full_domain" label="完整域名" min-width="200">
<template #default="scope">
<el-link
type="primary"
:href="'http://' + scope.row.full_domain"
target="_blank"
>
{{ scope.row.full_domain }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.status === 0" type="warning">审核中</el-tag>
<el-tag v-else-if="scope.row.status === 1" type="success"
>已生效</el-tag
>
<el-tag v-else type="danger">已禁用</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="申请时间" width="180" />
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="scope">
<el-button
size="small"
text
type="primary"
@click="handleCopy(scope.row.full_domain)"
>
复制
</el-button>
<el-button
size="small"
text
type="danger"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 申请域名弹窗 -->
<el-dialog v-model="dialogVisible" title="申请二级域名" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="选择主域名" prop="main_domain">
<el-select
v-model="form.main_domain"
placeholder="请选择主域名"
style="width: 100%"
>
<el-option
v-for="item in domainList"
:key="item.main_domain"
:label="item.main_domain"
:value="item.main_domain"
/>
</el-select>
</el-form-item>
<el-form-item label="二级前缀" prop="sub_domain">
<el-input v-model="form.sub_domain" placeholder="请输入二级域名前缀">
<template #append>{{
form.main_domain ? "." + form.main_domain : ""
}}</template>
</el-input>
<div class="form-tip">
只能包含字母数字和连字符不能以连字符开头或结尾
</div>
</el-form-item>
<el-form-item label="预览">
<div class="domain-preview">
{{
form.sub_domain
? form.sub_domain + "." + (form.main_domain || "example.com")
: "请填写上方信息"
}}
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading"
>提交申请</el-button
>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import {
getMyDomains,
applyTenantDomain,
getEnabledDomains,
deleteTenantDomain,
} from "@/api/domain";
import { useAuthStore } from "@/stores/auth";
const authStore = useAuthStore();
const loading = ref(false);
const submitLoading = ref(false);
const dialogVisible = ref(false);
const formRef = ref();
const tableData = ref<any[]>([]);
const domainList = ref<any[]>([]);
const form = reactive({
main_domain: "",
sub_domain: "",
});
const rules = {
main_domain: [{ required: true, message: "请选择主域名", trigger: "change" }],
sub_domain: [
{ required: true, message: "请输入二级域名前缀", trigger: "blur" },
{
pattern: /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/,
message: "格式不正确",
trigger: "blur",
},
],
};
const fetchDomains = async () => {
loading.value = true;
try {
// 获取我的域名列表
const res = await getMyDomains({ tid: authStore.user.tid });
if (res.code === 200) {
tableData.value = res.data || [];
}
// 获取可选主域名
const domainRes = await getEnabledDomains();
if (domainRes.code === 200) {
domainList.value = domainRes.data || [];
}
} finally {
loading.value = false;
}
};
const handleSubmit = async () => {
await formRef.value.validate();
submitLoading.value = true;
try {
const res = await applyTenantDomain({
tid: authStore.user.tid,
main_domain: form.main_domain,
sub_domain: form.sub_domain,
});
if (res.code === 200) {
ElMessage.success(res.msg);
dialogVisible.value = false;
form.sub_domain = "";
fetchDomains();
} else {
ElMessage.error(res.msg || "申请失败");
}
} finally {
submitLoading.value = false;
}
};
const handleCopy = (domain: string) => {
navigator.clipboard.writeText("http://" + domain);
ElMessage.success("链接已复制到剪贴板");
};
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(
`确定要删除域名 "${row.full_domain}" 吗?`,
"提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}
);
const res = await deleteTenantDomain(row.id);
if (res.code === 200) {
ElMessage.success("删除成功");
fetchDomains();
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (error: any) {
if (error !== "cancel") {
console.error("删除失败:", error);
}
}
};
onMounted(() => {
fetchDomains();
});
</script>
<style lang="less" scoped>
.container-box {
padding: 20px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
}
.form-tip {
font-size: 12px;
color: #999;
margin-top: 5px;
}
.domain-preview {
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
font-size: 14px;
color: #409eff;
}
</style>
@@ -0,0 +1,299 @@
<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"
>
<template #default="scope">
<span class="name-link" @click="handlePreview(scope.row)">
{{ scope.row.tenant_name }}
</span>
</template>
</el-table-column>
<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="160" align="center" fixed="right">
<template #default="scope">
<div class="action-icons">
<el-tooltip content="编辑" placement="top">
<el-button text size="small" @click="editRef.open(scope.row.id)">
<el-icon><Edit /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button
text
size="small"
type="danger"
@click="handleDelete(scope.row)"
>
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<EditModal ref="editRef" @success="refresh" />
<DetailDrawer ref="detailRef" />
<Qualification ref="qualificationRef" />
<!-- 分页 -->
<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 { ElMessage, ElMessageBox } from "element-plus";
import EditModal from "./components/edit.vue";
import DetailDrawer from "./components/detail.vue";
import Qualification from "./components/qualification.vue";
import { Edit, Delete } from "@element-plus/icons-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 detailRef = ref();
const qualificationRef = 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: any) => {
detailRef.value.open(row.id);
};
// 资质
const handleQualification = (row: any) => {
qualificationRef.value.open(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;
}
.action-icons {
display: inline-flex;
align-items: center;
gap: 6px;
}
</style>
@@ -0,0 +1,198 @@
<template>
<el-dialog v-model="visible" title="修改密码" width="400px" @close="handleClose">
<el-form :model="form">
<!-- 用户账号只读 -->
<el-form-item label="账号">
<el-input v-model="form.username" disabled />
</el-form-item>
<!-- 新密码 -->
<el-form-item label="新密码">
<el-input
v-model="passwordForm.newPassword"
type="password"
autocomplete="new-password"
show-password
placeholder="请输入新密码(6-16位)"
/>
</el-form-item>
<!-- 确认密码 -->
<el-form-item label="确认密码">
<el-input
v-model="passwordForm.confirmPassword"
type="password"
autocomplete="new-password"
show-password
placeholder="请再次输入新密码"
/>
</el-form-item>
<!-- 错误提示 -->
<el-form-item v-if="passwordError">
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
</el-form-item>
</el-form>
<!-- 对话框脚部 -->
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定修改</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { changePassword } from "@/api/user";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
userId: {
type: Number,
default: null,
},
});
const emit = defineEmits(['update:modelValue', 'submit', 'close']);
const visible = ref(false);
const passwordError = ref("");
const form = ref<any>({
id: null,
username: "",
});
const passwordForm = ref<any>({
newPassword: "",
confirmPassword: "",
});
// 监听 modelValue
watch(() => props.modelValue, (newVal) => {
visible.value = newVal;
if (newVal && props.userId) {
form.value.id = props.userId;
}
});
// 监听 userId 变化
watch(() => props.userId, (newVal) => {
if (newVal) {
form.value.id = newVal;
}
});
// 监听 visible 变化
watch(visible, (newVal) => {
if (!newVal) {
emit('update:modelValue', false);
}
});
// 校验密码
const validatePassword = (password: string) => {
if (!password) {
return "请输入密码";
}
if (password.length < 6) {
return "密码长度不能小于6位";
}
if (password.length > 16) {
return "密码长度不能大于16位";
}
return true;
};
// 校验确认密码
const validateConfirmPassword = (password: string, confirmPassword: string) => {
if (!confirmPassword) {
return "请再次输入密码";
}
if (confirmPassword !== password) {
return "两次输入的密码不一致";
}
return true;
};
const handleCancel = () => {
visible.value = false;
};
const handleClose = () => {
passwordForm.value = {
newPassword: "",
confirmPassword: "",
};
passwordError.value = "";
emit('close');
};
const handleSubmit = async () => {
// 清除之前的错误
passwordError.value = "";
try {
if (!form.value.id) {
passwordError.value = "用户ID不能为空";
return;
}
// 校验新密码格式
const passwordCheck = validatePassword(passwordForm.value.newPassword);
if (passwordCheck !== true) {
passwordError.value = passwordCheck;
return;
}
// 校验确认密码
const confirmCheck = validateConfirmPassword(
passwordForm.value.newPassword,
passwordForm.value.confirmPassword
);
if (confirmCheck !== true) {
passwordError.value = confirmCheck;
return;
}
// 调用接口修改密码
const res = await changePassword(form.value.id, passwordForm.value);
if (res.code === 200 || res.msg === '修改成功') {
ElMessage.success("密码修改成功");
visible.value = false;
emit('update:modelValue', false);
emit('submit');
} else {
passwordError.value = res.msg || "密码修改失败";
}
} catch (e: any) {
const errorMsg = e?.response?.data?.msg || e?.response?.data?.message || e?.message || "操作失败";
passwordError.value = errorMsg;
}
};
// 暴露方法给父组件
defineExpose({
open: (userId: number, username: string) => {
form.value = {
id: userId,
username: username,
};
passwordForm.value = {
newPassword: "",
confirmPassword: "",
};
passwordError.value = "";
visible.value = true;
},
});
</script>
<style lang="less" scoped>
</style>
@@ -0,0 +1,190 @@
<template>
<el-drawer v-model="visible" title="用户信息预览" size="50%">
<div class="user-preview" v-if="user">
<div class="user-header">
<div class="user-avatar">
<el-avatar :size="80" :icon="UserFilled" />
</div>
<h2 class="user-name">{{ user.name || "未知用户" }}</h2>
<el-tag :type="user.status === 1 ? 'success' : 'danger'" size="large">
{{ user.status === 1 ? "启用" : "禁用" }}
</el-tag>
</div>
<el-divider />
<div class="user-info">
<el-descriptions :column="2" border>
<el-descriptions-item label="ID">
{{ user.id }}
</el-descriptions-item>
<el-descriptions-item label="账号">
{{ user.account || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="姓名">
{{ user.name || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="性别">
{{ user.sex === 1 ? "男" : "女" }}
</el-descriptions-item>
<el-descriptions-item label="手机号">
{{ user.phone || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="QQ">
{{ user.qq || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="邮箱">
{{ user.email || "未设置" }}
</el-descriptions-item>
<el-descriptions-item label="角色">
<el-tag :type="getRoleTagType(user.rid)" size="small">
{{ getRoleName(user.rid) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="最后登录IP">
{{ user.last_login_ip || "未登录" }}
</el-descriptions-item>
<el-descriptions-item label="登录次数">
{{ user.login_count || 0 }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ user.create_time || "未知" }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ user.update_time || "未知" }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
<div v-else class="no-user">
<el-empty description="暂无用户信息" />
</div>
</el-drawer>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { UserFilled } from "@element-plus/icons-vue";
import { getAllRoles } from "@/api/role";
interface User {
id: number;
account: string;
name: string;
phone: string;
qq: string;
email: string;
sex: number;
rid: number;
status: number;
last_login_ip: string;
login_count: number;
create_time: string;
update_time: string;
}
interface Role {
id: number;
name: string;
}
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
user: {
type: Object as () => User | undefined,
default: undefined,
},
});
const emit = defineEmits(["update:modelValue"]);
const visible = ref(false);
const roles = ref<Role[]>([]);
// 监听对话框显示状态
watch(
() => props.modelValue,
(newVal) => {
visible.value = newVal;
}
);
// 监听visible变化,同步给父组件
watch(visible, (newVal) => {
emit("update:modelValue", newVal);
});
// 获取角色列表
const fetchRoles = async () => {
try {
const res = await getAllRoles();
roles.value = res.data || [];
} catch (e) {
roles.value = [];
}
};
// 获取角色tag状态
function getRoleTagType(rid: number): string {
const typeMap: Record<number, string> = {
1: "primary",
2: "success",
3: "warning",
4: "danger",
};
return typeMap[rid] || "primary";
}
// 获取角色名称
function getRoleName(rid: number): string {
const role = roles.value.find((r) => r.id === rid);
return role?.name || "未知";
}
// 暴露open方法供父组件调用
const open = (userData?: User) => {
visible.value = true;
fetchRoles();
};
defineExpose({
open,
});
// 初始化时获取角色列表
fetchRoles();
</script>
<style lang="less" scoped>
.user-preview {
padding: 20px;
.user-header {
text-align: center;
padding: 20px 0;
.user-avatar {
margin-bottom: 15px;
}
.user-name {
margin: 15px 0;
font-size: 24px;
font-weight: 600;
color: #303133;
}
}
.user-info {
margin-top: 20px;
}
}
.no-user {
padding: 40px 0;
text-align: center;
}
</style>
@@ -0,0 +1,443 @@
<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="角色" prop="rid">
<el-select v-model="form.rid" placeholder="请选择角色" style="width: 100%">
<el-option
v-for="role in roleOptions"
:key="role.id"
:label="role.name"
:value="Number(role.id)"
/>
</el-select>
</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";
import { getAllRoles } from "@/api/role";
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: "",
rid: undefined,
sex: 1,
password: "",
confirmPassword: "",
email: "",
status: 1,
});
const roleOptions = ref<any[]>([]);
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" }],
rid: [{ required: true, message: "请选择角色", trigger: "change" }],
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,
rid: data.rid,
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 loadRoles = async () => {
try {
const res = await getAllRoles();
const allRoles = Array.isArray(res?.data) ? res.data : [];
// 平台用户只能绑定平台角色
roleOptions.value = allRoles.filter((r: any) => Number(r.cid || 1) === 1);
} catch {
roleOptions.value = [];
}
};
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,
rid: form.value.rid,
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,
rid: form.value.rid,
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;
loadRoles();
form.value = {
id: 0,
account: "",
name: "",
phone: "",
qq: "",
rid: undefined,
sex: 1,
password: "",
confirmPassword: "",
email: "",
status: 1,
};
visible.value = true;
// 清除表单验证
if (formRef.value) {
formRef.value.clearValidate();
}
},
openEdit: (user: any) => {
isAdd.value = false;
loadRoles();
visible.value = true;
// 清除表单验证
if (formRef.value) {
formRef.value.clearValidate();
}
// 异步加载用户详细信息
loadUserData(user);
},
open: (user?: any) => {
loadRoles();
if (user) {
isAdd.value = false;
loadUserData(user);
} else {
isAdd.value = true;
form.value = {
id: 0,
account: "",
name: "",
phone: "",
qq: "",
rid: undefined,
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,0 +1,296 @@
<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="name" label="姓名" min-width="120" 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="rid" label="角色" align="center">
<template #default="scope">
<el-tag :type="getRoleTagType(scope.row.rid)" size="small">
{{ getRoleTagText(roles, scope.row.rid) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="account" label="账号" min-width="180" align="center" />
<el-table-column prop="phone" label="手机号" min-width="180" align="center" />
<el-table-column prop="qq" label="QQ" min-width="180" align="center" />
<el-table-column prop="last_login_ip" label="最后登录IP" width="120" align="center" />
<el-table-column prop="login_count" label="登陆次数" width="120" 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="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>
<!-- 编辑用户对话框组件 -->
<UserEditDialog ref="userEditRef" :modelValue="editDialogVisible" @update:modelValue="editDialogVisible = $event"
:is-edit="isEdit" @submit="handleEditSuccess" @close="editDialogVisible = false" />
<!-- 修改密码对话框组件 -->
<ChangePasswordDialog ref="changePasswordRef" :modelValue="passwordDialogVisible"
@update:modelValue="passwordDialogVisible = $event" :user-id="currentUserId" @submit="handlePasswordChangeSuccess"
@close="passwordDialogVisible = false" />
<!-- 预览用户对话框组件 -->
<PreviewDialog ref="previewDialogRef" :modelValue="previewDialogVisible"
@update:modelValue="previewDialogVisible = $event" :user="currentUser" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Refresh } from "@element-plus/icons-vue";
import { getAllUsers, deleteUser } from "@/api/user";
import { getAllRoles } from "@/api/role";
import UserEditDialog from "./components/userEdit.vue";
import ChangePasswordDialog from "./components/changePassword.vue";
import PreviewDialog from "./components/preview.vue";
interface User {
id: number;
tid?: number;
account: string;
name: string;
phone: string;
qq: string;
sex: number;
rid: number;
status: number;
last_login_ip: string;
login_count: number;
create_time: string;
update_time: string;
}
interface Role {
id: number;
name: string;
status?: number;
rights?: string;
create_time?: string;
update_time?: string;
}
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const users = ref<User[]>([]);
const roles = ref<Role[]>([]);
const loading = ref(false);
// 组件引用
const userEditRef = ref();
const changePasswordRef = ref();
const previewDialogRef = ref();
// 编辑/密码对话框状态
const editDialogVisible = ref(false);
const passwordDialogVisible = ref(false);
const previewDialogVisible = ref(false);
const editDialogTitle = ref("添加用户");
const isEdit = ref(false);
const currentUserId = ref<number | undefined>(undefined);
const currentUser = ref<User | undefined>(undefined);
//刷新
const refresh = async () => {
await fetchUsers();
};
// 获取角色列表
const fetchRoles = async () => {
try {
const res = await getAllRoles();
roles.value = res.data || [];
} catch (e) {
roles.value = [];
}
};
// 获取角色tag状态
function getRoleTagType(rid: number): string {
const typeMap: Record<number, string> = {
1: "primary",
2: "success",
3: "warning",
4: "danger",
};
return typeMap[rid] || "primary";
}
// 获取角色tag文本
function getRoleTagText(roles: Role[] | undefined, rid: number): string {
if (!roles || !Array.isArray(roles)) {
return "未知";
}
return roles.find((role) => role.id === rid)?.name || "未知";
}
// 获取用户列表
const fetchUsers = async () => {
loading.value = true;
try {
const res = await getAllUsers();
users.value = res?.data?.list || [];
total.value = Number(res?.data?.total || 0);
} catch (e) {
users.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
// 添加用户
const handleAddUser = () => {
isEdit.value = false;
editDialogVisible.value = true;
if (userEditRef.value) {
userEditRef.value.open();
}
};
// 编辑用户
const handleEdit = (user: User) => {
isEdit.value = true;
editDialogVisible.value = true;
if (userEditRef.value) {
userEditRef.value.open(user);
}
};
// 预览用户
const handlePreview = (user: User) => {
currentUser.value = user;
previewDialogVisible.value = true;
if (previewDialogRef.value) {
previewDialogRef.value.open(user);
}
};
//修改密码
const handleChangePassword = async (user: User) => {
changePasswordRef.value.open(user.id, user.account);
passwordDialogVisible.value = true;
currentUserId.value = user.id;
};
// 编辑成功回调
const handleEditSuccess = () => {
editDialogVisible.value = false;
ElMessage.success(isEdit.value ? "编辑成功" : "添加成功");
fetchUsers();
};
// 密码修改成功回调
const handlePasswordChangeSuccess = () => {
passwordDialogVisible.value = false;
ElMessage.success("密码修改成功");
};
// 分页改变
const handlePageChange = (val: number) => {
page.value = val;
fetchUsers();
};
// 删除用户
const handleDelete = async (user: User) => {
ElMessageBox.confirm("确认删除该用户?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
await deleteUser(user.id);
ElMessage.success("删除成功");
fetchUsers();
} catch (e) {
ElMessage.error("删除失败");
}
});
};
onMounted(async () => {
fetchUsers();
fetchRoles();
});
</script>
<style lang="less" scoped>
.filter-bar {
margin-bottom: 12px;
}
.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>