前端代码格式化

This commit is contained in:
2026-02-19 11:46:27 +08:00
parent d310a29c03
commit f0f0763ceb
101 changed files with 8952 additions and 13203 deletions
@@ -1,61 +1,20 @@
<template>
<a-modal
:title="titleMap[mode]"
:open="visible"
:width="500"
:destroy-on-close="true"
:footer="null"
@cancel="handleCancel"
>
<a-form
:model="form"
:rules="rules"
:disabled="mode === 'show'"
ref="dialogForm"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
>
<a-modal :title="titleMap[mode]" :open="visible" :width="500" :destroy-on-close="true" :footer="null" @cancel="handleCancel">
<a-form :model="form" :rules="rules" :disabled="mode === 'show'" ref="dialogForm" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-form-item label="上级部门" name="parent_id">
<a-tree-select
v-model:value="form.parent_id"
:tree-data="filteredDepartments"
:field-names="departmentFieldNames"
:tree-default-expand-all="false"
placeholder="请选择上级部门"
allow-clear
tree-node-filter-prop="name"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
/>
<a-tree-select v-model:value="form.parent_id" :tree-data="filteredDepartments" :field-names="departmentFieldNames" :tree-default-expand-all="false" placeholder="请选择上级部门" allow-clear tree-node-filter-prop="name" :dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" />
</a-form-item>
<a-form-item label="部门名称" name="name">
<a-input
v-model:value="form.name"
placeholder="请输入部门名称"
allow-clear
></a-input>
<a-input v-model:value="form.name" placeholder="请输入部门名称" allow-clear></a-input>
</a-form-item>
<a-form-item label="负责人" name="leader">
<a-input
v-model:value="form.leader"
placeholder="请输入负责人"
allow-clear
></a-input>
<a-input v-model:value="form.leader" placeholder="请输入负责人" allow-clear></a-input>
</a-form-item>
<a-form-item label="联系电话" name="phone">
<a-input
v-model:value="form.phone"
placeholder="请输入联系电话"
allow-clear
></a-input>
<a-input v-model:value="form.phone" placeholder="请输入联系电话" allow-clear></a-input>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number
v-model:value="form.sort"
:min="0"
:max="10000"
style="width: 100%"
placeholder="请输入排序"
/>
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" placeholder="请输入排序" />
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="form.status">
@@ -65,13 +24,7 @@
</a-form-item>
<a-form-item :wrapper-col="{ offset: 5 }">
<div style="display: flex; gap: 10px">
<a-button
v-if="mode !== 'show'"
type="primary"
:loading="isSaveing"
@click="submit"
> </a-button
>
<a-button v-if="mode !== 'show'" type="primary" :loading="isSaveing" @click="submit"> </a-button>
<a-button @click="handleCancel"> </a-button>
</div>
</a-form-item>
@@ -80,66 +33,66 @@
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { message } from "ant-design-vue";
import authApi from "@/api/auth";
import { ref, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
defineOptions({
name: "DepartmentSaveDialog",
});
name: 'DepartmentSaveDialog',
})
const emit = defineEmits(["success", "closed"]);
const emit = defineEmits(['success', 'closed'])
const mode = ref("add");
const mode = ref('add')
const titleMap = {
add: "新增部门",
edit: "编辑部门",
show: "查看部门",
};
const visible = ref(false);
const isSaveing = ref(false);
add: '新增部门',
edit: '编辑部门',
show: '查看部门',
}
const visible = ref(false)
const isSaveing = ref(false)
// 表单数据
const form = reactive({
id: "",
name: "",
leader: "",
phone: "",
id: '',
name: '',
leader: '',
phone: '',
sort: 0,
parent_id: null,
status: 1,
});
})
// 表单引用
const dialogForm = ref();
const dialogForm = ref()
// 验证规则
const rules = {
name: [{ required: true, message: "请输入部门名称", trigger: "blur" }],
name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
sort: [
{ required: true, message: "请输入排序", trigger: "change" },
{ type: "number", message: "排序必须为数字", trigger: "change" },
{ required: true, message: '请输入排序', trigger: 'change' },
{ type: 'number', message: '排序必须为数字', trigger: 'change' },
],
};
}
// 部门数据
const departments = ref([]);
const departments = ref([])
const departmentFieldNames = {
label: "name",
value: "id",
children: "children",
};
label: 'name',
value: 'id',
children: 'children',
}
// 当前编辑的部门ID(用于过滤树)
const currentEditId = ref(null);
const currentEditId = ref(null)
// 过滤后的部门树(编辑时排除自己和子部门)
const filteredDepartments = computed(() => {
if (mode.value === "add") {
return departments.value;
if (mode.value === 'add') {
return departments.value
}
return filterDepartments(departments.value, currentEditId.value);
});
return filterDepartments(departments.value, currentEditId.value)
})
// 递归过滤部门树
const filterDepartments = (tree, excludeId) => {
@@ -147,96 +100,94 @@ const filterDepartments = (tree, excludeId) => {
.filter((item) => item.id !== excludeId)
.map((item) => ({
...item,
children: item.children
? filterDepartments(item.children, excludeId)
: undefined,
}));
};
children: item.children ? filterDepartments(item.children, excludeId) : undefined,
}))
}
// 显示对话框
const open = (openMode = "add") => {
mode.value = openMode;
visible.value = true;
const open = (openMode = 'add') => {
mode.value = openMode
visible.value = true
return {
setData,
open,
close,
};
};
}
}
// 关闭对话框
const close = () => {
visible.value = false;
};
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit("closed");
visible.value = false;
};
emit('closed')
visible.value = false
}
// 表单提交方法
const submit = async () => {
try {
await dialogForm.value.validate();
isSaveing.value = true;
let res = {};
form.parent_id = form.parent_id || 0;
await dialogForm.value.validate()
isSaveing.value = true
let res = {}
form.parent_id = form.parent_id || 0
if (mode.value === "add") {
res = await authApi.departments.add.post(form);
if (mode.value === 'add') {
res = await authApi.department.add.post(form)
} else {
res = await authApi.departments.edit.put(form.id, form);
res = await authApi.department.edit.put(form.id, form)
}
isSaveing.value = false;
isSaveing.value = false
if (res.code === 200) {
emit("success", form, mode.value);
visible.value = false;
message.success("操作成功");
emit('success', form, mode.value)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("表单验证失败", error);
isSaveing.value = false;
console.error('表单验证失败', error)
isSaveing.value = false
}
};
}
// 加载部门树数据
const loadDepartments = async () => {
try {
const res = await authApi.departments.tree.get();
const res = await authApi.department.tree.get()
if (res.code === 200) {
departments.value = res.data || [];
departments.value = res.data || []
}
} catch (error) {
console.error("加载部门树失败:", error);
message.error("加载部门树失败");
console.error('加载部门树失败:', error)
message.error('加载部门树失败')
}
};
}
// 表单注入数据
const setData = (data) => {
form.id = data.id;
currentEditId.value = data.id;
form.name = data.name;
form.leader = data.leader || "";
form.phone = data.phone || "";
form.sort = data.sort || 0;
form.parent_id = data.parent_id || null;
form.status = data.status !== undefined ? data.status : 1;
};
form.id = data.id
currentEditId.value = data.id
form.name = data.name
form.leader = data.leader || ''
form.phone = data.phone || ''
form.sort = data.sort || 0
form.parent_id = data.parent_id || null
form.status = data.status !== undefined ? data.status : 1
}
// 组件挂载时加载数据
loadDepartments();
loadDepartments()
// 暴露方法给父组件
defineExpose({
open,
setData,
close,
});
})
</script>
<style></style>
@@ -4,18 +4,8 @@
<div class="tool-bar">
<div class="left-panel">
<a-space>
<a-input
v-model:value="searchForm.keyword"
placeholder="部门名称"
allow-clear
style="width: 200px"
/>
<a-select
v-model:value="searchForm.status"
placeholder="状态"
allow-clear
style="width: 120px"
>
<a-input v-model:value="searchForm.keyword" placeholder="部门名称" allow-clear style="width: 200px" />
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 120px">
<a-select-option :value="1">正常</a-select-option>
<a-select-option :value="0">禁用</a-select-option>
</a-select>
@@ -35,17 +25,11 @@
<template #icon><plus-outlined /></template>
新增
</a-button>
<a-button
:disabled="selectedRows.length === 0"
@click="handleBatchStatus(1)"
>
<a-button :disabled="selectedRows.length === 0" @click="handleBatchStatus(1)">
<template #icon><check-circle-outlined /></template>
启用
</a-button>
<a-button
:disabled="selectedRows.length === 0"
@click="handleBatchStatus(0)"
>
<a-button :disabled="selectedRows.length === 0" @click="handleBatchStatus(0)">
<template #icon><stop-outlined /></template>
禁用
</a-button>
@@ -57,22 +41,16 @@
<template #overlay>
<a-menu>
<a-menu-item @click="handleExport">
<template #icon
><download-outlined
/></template>
<template #icon><download-outlined /></template>
导出
</a-menu-item>
<a-menu-item @click="handleImport">
<template #icon
><upload-outlined
/></template>
<template #icon><upload-outlined /></template>
导入
</a-menu-item>
<a-menu-divider />
<a-menu-item danger @click="handleBatchDelete">
<template #icon
><delete-outlined
/></template>
<template #icon><delete-outlined /></template>
批量删除
</a-menu-item>
</a-menu>
@@ -84,46 +62,23 @@
<!-- 表格内容区域 -->
<div class="table-content">
<scTable
ref="tableRef"
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="false"
:row-key="rowKey"
:row-selection="rowSelection"
:default-expand-all="true"
@refresh="refreshTable"
@select="handleSelectChange"
@selectAll="handleSelectAll"
>
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" :pagination="false" :row-key="rowKey" :row-selection="rowSelection" :default-expand-all="true" @refresh="refreshTable" @select="handleSelectChange" @selectAll="handleSelectAll">
<template #status="{ record }">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? "正常" : "禁用" }}
{{ record.status === 1 ? '正常' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button
type="link"
size="small"
@click="handleView(record)"
>
<a-button type="link" size="small" @click="handleView(record)">
<template #icon><eye-outlined /></template>
查看
</a-button>
<a-button
type="link"
size="small"
@click="handleEdit(record)"
>
<a-button type="link" size="small" @click="handleEdit(record)">
<template #icon><edit-outlined /></template>
编辑
</a-button>
<a-popconfirm
title="确定删除该部门吗?如果该部门下有子部门或用户,将无法删除"
@confirm="handleDelete(record)"
>
<a-popconfirm title="确定删除该部门吗?如果该部门下有子部门或用户,将无法删除" @confirm="handleDelete(record)">
<a-button type="link" size="small" danger>
<template #icon><delete-outlined /></template>
删除
@@ -136,294 +91,262 @@
</div>
<!-- 新增/编辑/查看部门弹窗 -->
<save-dialog
v-if="dialog.save"
ref="saveDialogRef"
@success="handleSaveSuccess"
@closed="dialog.save = false"
/>
<save-dialog v-if="dialog.save" ref="saveDialogRef" @success="handleSaveSuccess" @closed="dialog.save = false" />
<!-- 导入部门弹窗 -->
<sc-import
v-model:open="dialog.import"
title="导入部门"
:api="authApi.departments.import.post"
:template-api="authApi.departments.downloadTemplate.get"
filename="部门"
@success="handleImportSuccess"
/>
<sc-import v-model:open="dialog.import" title="导入部门" :api="authApi.department.import.post" :template-api="authApi.department.downloadTemplate.get" filename="部门" @success="handleImportSuccess" />
<!-- 导出部门弹窗 -->
<sc-export
v-model:open="dialog.export"
title="导出部门"
:api="handleExportApi"
:default-filename="`部门列表_${Date.now()}`"
:show-options="false"
tip="导出当前选中或所有部门数据"
@success="handleExportSuccess"
/>
<sc-export v-model:open="dialog.export" title="导出部门" :api="handleExportApi" :default-filename="`部门列表_${Date.now()}`" :show-options="false" tip="导出当前选中或所有部门数据" @success="handleExportSuccess" />
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { message, Modal } from "ant-design-vue";
import scTable from "@/components/scTable/index.vue";
import scImport from "@/components/scImport/index.vue";
import scExport from "@/components/scExport/index.vue";
import saveDialog from "./components/SaveDialog.vue";
import authApi from "@/api/auth";
import { useTable } from "@/hooks/useTable";
import { ref, reactive, onMounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import scTable from '@/components/scTable/index.vue'
import scImport from '@/components/scImport/index.vue'
import scExport from '@/components/scExport/index.vue'
import saveDialog from './components/SaveDialog.vue'
import authApi from '@/api/auth'
import { useTable } from '@/hooks/useTable'
defineOptions({
name: "authDepartment",
});
name: 'authDepartment',
})
// 使用useTable hooks
const {
tableRef,
searchForm,
tableData,
loading,
selectedRows,
rowSelection,
handleSearch,
handleReset,
handleSelectChange,
handleSelectAll,
refreshTable,
} = useTable({
api: authApi.departments.tree.get,
const { tableRef, searchForm, tableData, loading, selectedRows, rowSelection, handleSearch, handleReset, handleSelectChange, handleSelectAll, refreshTable } = useTable({
api: authApi.department.tree.get,
searchForm: {
keyword: "",
keyword: '',
status: null,
},
columns: [],
needPagination: false,
needSelection: true,
immediateLoad: false,
});
})
// 对话框状态
const dialog = reactive({
save: false,
import: false,
export: false,
});
})
// 弹窗引用
const saveDialogRef = ref(null);
const saveDialogRef = ref(null)
// 行key
const rowKey = "id";
const rowKey = 'id'
// 表格列配置
const columns = [
{
title: "#",
dataIndex: "_index",
key: "_index",
title: '#',
dataIndex: '_index',
key: '_index',
width: 60,
align: "center",
align: 'center',
},
{ title: "部门名称", dataIndex: "name", key: "name", width: 300 },
{ title: "负责人", dataIndex: "leader", key: "leader", width: 120 },
{ title: "联系电话", dataIndex: "phone", key: "phone", width: 150 },
{ title: '部门名称', dataIndex: 'name', key: 'name', width: 300 },
{ title: '负责人', dataIndex: 'leader', key: 'leader', width: 120 },
{ title: '联系电话', dataIndex: 'phone', key: 'phone', width: 150 },
{
title: "排序",
dataIndex: "sort",
key: "sort",
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 100,
align: "center",
align: 'center',
},
{
title: "状态",
dataIndex: "status",
key: "status",
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: "center",
slot: "status",
align: 'center',
slot: 'status',
},
{
title: "操作",
dataIndex: "action",
key: "action",
title: '操作',
dataIndex: 'action',
key: 'action',
width: 220,
align: "center",
slot: "action",
fixed: "right",
align: 'center',
slot: 'action',
fixed: 'right',
},
];
]
// 新增部门
const handleAdd = () => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("add");
}, 0);
};
saveDialogRef.value?.open('add')
}, 0)
}
// 查看部门
const handleView = (record) => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("show").setData(record);
}, 0);
};
saveDialogRef.value?.open('show').setData(record)
}, 0)
}
// 编辑部门
const handleEdit = (record) => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("edit").setData(record);
}, 0);
};
saveDialogRef.value?.open('edit').setData(record)
}, 0)
}
// 删除部门
const handleDelete = async (record) => {
try {
const res = await authApi.departments.delete.delete(record.id);
const res = await authApi.department.delete.delete(record.id)
if (res.code === 200) {
message.success("删除成功");
refreshTable();
message.success('删除成功')
refreshTable()
} else {
message.error(res.message || "删除失败");
message.error(res.message || '删除失败')
}
} catch (error) {
console.error("删除部门失败:", error);
console.error('删除部门失败:', error)
// 如果是验证错误,显示具体错误信息
if (error.response?.data?.message) {
message.error(error.response.data.message);
message.error(error.response.data.message)
} else {
message.error("删除失败");
message.error('删除失败')
}
}
};
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要删除的部门");
return;
message.warning('请选择要删除的部门')
return
}
Modal.confirm({
title: "确认删除",
title: '确认删除',
content: `确定删除选中的 ${selectedRows.value.length} 个部门吗?如果删除项中含有子集或用户,将会被一并删除`,
okText: "确定",
cancelText: "取消",
okType: "danger",
okText: '确定',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
try {
const ids = selectedRows.value.map((item) => item.id);
const res = await authApi.departments.batchDelete.post({ ids });
const ids = selectedRows.value.map((item) => item.id)
const res = await authApi.department.batchDelete.post({ ids })
if (res.code === 200) {
message.success(res.message || "删除成功");
selectedRows.value = [];
refreshTable();
message.success(res.message || '删除成功')
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || "删除失败");
message.error(res.message || '删除失败')
}
} catch (error) {
console.error("批量删除部门失败:", error);
console.error('批量删除部门失败:', error)
if (error.response?.data?.message) {
message.error(error.response.data.message);
message.error(error.response.data.message)
} else {
message.error("删除失败");
message.error('删除失败')
}
}
},
});
};
})
}
// 批量更新状态
const handleBatchStatus = (status) => {
if (selectedRows.value.length === 0) {
message.warning("请选择要操作的部门");
return;
message.warning('请选择要操作的部门')
return
}
Modal.confirm({
title: "确认操作",
content: `确定${status === 1 ? "启用" : "禁用"}选中的 ${selectedRows.value.length} 个部门吗?`,
okText: "确定",
cancelText: "取消",
title: '确认操作',
content: `确定${status === 1 ? '启用' : '禁用'}选中的 ${selectedRows.value.length} 个部门吗?`,
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
const ids = selectedRows.value.map((item) => item.id);
const res = await authApi.departments.batchStatus.post({
const ids = selectedRows.value.map((item) => item.id)
const res = await authApi.department.batchStatus.post({
ids,
status,
});
})
if (res.code === 200) {
message.success(res.message || "操作成功");
selectedRows.value = [];
refreshTable();
message.success(res.message || '操作成功')
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("批量更新状态失败:", error);
message.error("操作失败");
console.error('批量更新状态失败:', error)
message.error('操作失败')
}
},
});
};
})
}
// 导出部门
const handleExport = () => {
dialog.export = true;
};
dialog.export = true
}
// 导出API封装
const handleExportApi = async () => {
const ids = selectedRows.value.map((item) => item.id);
return await authApi.departments.export.post({
const ids = selectedRows.value.map((item) => item.id)
return await authApi.department.export.post({
ids: ids.length > 0 ? ids : undefined,
});
};
})
}
// 导出成功回调
const handleExportSuccess = () => {
selectedRows.value = [];
};
selectedRows.value = []
}
// 导入部门
const handleImport = () => {
dialog.import = true;
};
dialog.import = true
}
// 导入成功回调
const handleImportSuccess = () => {
refreshTable();
};
refreshTable()
}
// 下载模板
const handleDownloadTemplate = async () => {
try {
const blob = await authApi.departments.downloadTemplate.get();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "部门导入模板.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success("下载成功");
const blob = await authApi.department.downloadTemplate.get()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '部门导入模板.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('下载成功')
} catch (error) {
console.error("下载模板失败:", error);
message.error("下载失败");
console.error('下载模板失败:', error)
message.error('下载失败')
}
};
}
// 保存成功回调
const handleSaveSuccess = () => {
refreshTable();
};
refreshTable()
}
// 初始化
onMounted(() => {
refreshTable();
});
refreshTable()
})
</script>
@@ -5,11 +5,7 @@
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="在线用户总数"
:value="onlineCount"
:value-style="{ color: '#3f8600' }"
>
<a-statistic title="在线用户总数" :value="onlineCount" :value-style="{ color: '#3f8600' }">
<template #prefix>
<UserOutlined style="font-size: 24px" />
</template>
@@ -20,44 +16,22 @@
<a-card>
<a-form layout="inline" :model="searchForm">
<a-form-item label="刷新间隔">
<a-select
v-model:value="refreshInterval"
style="width: 150px"
@change="handleRefreshIntervalChange"
>
<a-select-option :value="0"
>不自动刷新</a-select-option
>
<a-select-option :value="5000"
>5</a-select-option
>
<a-select-option :value="10000"
>10</a-select-option
>
<a-select-option :value="30000"
>30</a-select-option
>
<a-select-option :value="60000"
>60</a-select-option
>
<a-select v-model:value="refreshInterval" style="width: 150px" @change="handleRefreshIntervalChange">
<a-select-option :value="0">不自动刷新</a-select-option>
<a-select-option :value="5000">5</a-select-option>
<a-select-option :value="10000">10</a-select-option>
<a-select-option :value="30000">30</a-select-option>
<a-select-option :value="60000">60</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-space>
<a-button
type="primary"
@click="handleRefresh"
:loading="loading"
>
<template #icon
><ReloadOutlined
/></template>
<a-button type="primary" @click="handleRefresh" :loading="loading">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
<a-button @click="handleRefreshAllOffline">
<template #icon
><StopOutlined
/></template>
<template #icon><StopOutlined /></template>
全部下线
</a-button>
</a-space>
@@ -72,12 +46,7 @@
<div class="tool-bar">
<div class="left-panel">
<a-space>
<a-input
v-model:value="searchForm.keyword"
placeholder="用户名"
allow-clear
style="width: 200px"
/>
<a-input v-model:value="searchForm.keyword" placeholder="用户名" allow-clear style="width: 200px" />
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
搜索
@@ -92,18 +61,10 @@
<!-- 表格内容 -->
<div class="table-content">
<sc-table
ref="tableRef"
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
:row-key="rowKey"
@refresh="refreshTable"
>
<sc-table ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" :pagination="pagination" :row-key="rowKey" @refresh="refreshTable">
<template #status="{ record }">
<a-tag :color="record.is_online ? 'success' : 'default'">
{{ record.is_online ? "在线" : "离线" }}
{{ record.is_online ? '在线' : '离线' }}
</a-tag>
</template>
<template #lastActive="{ record }">
@@ -111,73 +72,44 @@
</template>
<template #action="{ record }">
<a-space>
<a-button
type="link"
size="small"
@click="handleViewSessions(record)"
>
查看会话
</a-button>
<a-popconfirm
title="确定强制该用户下线吗?"
@confirm="handleOffline(record)"
>
<a-button type="link" size="small" danger>
强制下线
</a-button>
<a-button type="link" size="small" @click="handleViewSessions(record)"> 查看会话 </a-button>
<a-popconfirm title="确定强制该用户下线吗?" @confirm="handleOffline(record)">
<a-button type="link" size="small" danger> 强制下线 </a-button>
</a-popconfirm>
<a-button
type="link"
size="small"
danger
@click="handleOfflineAll(record)"
>
全部下线
</a-button>
<a-button type="link" size="small" danger @click="handleOfflineAll(record)"> 全部下线 </a-button>
</a-space>
</template>
</sc-table>
</div>
<!-- 会话详情弹窗 -->
<sessions-dialog
v-if="dialog.sessions"
ref="sessionsDialogRef"
@success="handleSessionsSuccess"
@closed="dialog.sessions = false"
/>
<sessions-dialog v-if="dialog.sessions" ref="sessionsDialogRef" @success="handleSessionsSuccess" @closed="dialog.sessions = false" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from "vue";
import { message, Modal } from "ant-design-vue";
import {
UserOutlined,
SearchOutlined,
RedoOutlined,
ReloadOutlined,
StopOutlined,
} from "@ant-design/icons-vue";
import scTable from "@/components/scTable/index.vue";
import sessionsDialog from "./sessions.vue";
import authApi from "@/api/auth";
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { UserOutlined, SearchOutlined, RedoOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons-vue'
import scTable from '@/components/scTable/index.vue'
import sessionsDialog from './sessions.vue'
import authApi from '@/api/auth'
defineOptions({
name: "authOnlineUsers",
});
name: 'authOnlineUsers',
})
// 表格引用
const tableRef = ref(null);
const tableRef = ref(null)
// 搜索表单
const searchForm = reactive({
keyword: "",
});
keyword: '',
})
// 表格数据
const tableData = ref([]);
const loading = ref(false);
const tableData = ref([])
const loading = ref(false)
const pagination = reactive({
current: 1,
pageSize: 20,
@@ -185,257 +117,254 @@ const pagination = reactive({
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
});
})
// 行key
const rowKey = "id";
const rowKey = 'id'
// 在线用户数量
const onlineCount = ref(0);
const onlineCount = ref(0)
// 刷新定时器
const refreshInterval = ref(30000); // 默认30秒
let refreshTimer = null;
const refreshInterval = ref(30000) // 默认30秒
let refreshTimer = null
// 对话框状态
const dialog = reactive({
sessions: false,
});
})
// 弹窗引用
const sessionsDialogRef = ref(null);
const sessionsDialogRef = ref(null)
// 表格列配置
const columns = [
{
title: "#",
dataIndex: "_index",
key: "_index",
title: '#',
dataIndex: '_index',
key: '_index',
width: 60,
align: "center",
align: 'center',
},
{ title: "用户名", dataIndex: "username", key: "username", width: 150 },
{ title: "真实姓名", dataIndex: "real_name", key: "real_name", width: 150 },
{ title: "邮箱", dataIndex: "email", key: "email", width: 200 },
{ title: "手机号", dataIndex: "phone", key: "phone", width: 150 },
{ title: '用户名', dataIndex: 'username', key: 'username', width: 150 },
{ title: '真实姓名', dataIndex: 'real_name', key: 'real_name', width: 150 },
{ title: '邮箱', dataIndex: 'email', key: 'email', width: 200 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 150 },
{
title: "状态",
dataIndex: "status",
key: "status",
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: "center",
slot: "status",
align: 'center',
slot: 'status',
},
{
title: "最后活跃时间",
dataIndex: "last_active_at",
key: "last_active_at",
title: '最后活跃时间',
dataIndex: 'last_active_at',
key: 'last_active_at',
width: 180,
slot: "lastActive",
slot: 'lastActive',
},
{
title: "最后登录IP",
dataIndex: "last_login_ip",
key: "last_login_ip",
title: '最后登录IP',
dataIndex: 'last_login_ip',
key: 'last_login_ip',
width: 150,
},
{
title: "操作",
dataIndex: "action",
key: "action",
title: '操作',
dataIndex: 'action',
key: 'action',
width: 200,
align: "center",
slot: "action",
fixed: "right",
align: 'center',
slot: 'action',
fixed: 'right',
},
];
]
// 加载在线用户数量
const loadOnlineCount = async () => {
try {
const res = await authApi.onlineUsers.count.get();
const res = await authApi.onlineUser.count.get()
if (res.code === 200) {
onlineCount.value = res.data || 0;
onlineCount.value = res.data || 0
}
} catch (error) {
console.error("获取在线用户数量失败:", error);
console.error('获取在线用户数量失败:', error)
}
};
}
// 加载在线用户列表
const loadOnlineUsers = async () => {
try {
loading.value = true;
loading.value = true
const params = {
...searchForm,
limit: pagination.pageSize,
};
const res = await authApi.onlineUsers.list.get(params);
loading.value = false;
}
const res = await authApi.onlineUser.list.get(params)
loading.value = false
if (res.code === 200) {
// 添加序号
const list = res.data?.list || [];
const list = res.data?.list || []
tableData.value = list.map((item, index) => ({
...item,
_index:
(pagination.current - 1) * pagination.pageSize + index + 1,
}));
pagination.total = res.data?.total || 0;
_index: (pagination.current - 1) * pagination.pageSize + index + 1,
}))
pagination.total = res.data?.total || 0
}
} catch (error) {
console.error("加载在线用户列表失败:", error);
loading.value = false;
console.error('加载在线用户列表失败:', error)
loading.value = false
}
};
}
// 刷新表格
const refreshTable = () => {
loadOnlineCount();
loadOnlineUsers();
};
loadOnlineCount()
loadOnlineUsers()
}
// 搜索
const handleSearch = () => {
pagination.current = 1;
refreshTable();
};
pagination.current = 1
refreshTable()
}
// 重置
const handleReset = () => {
searchForm.keyword = "";
pagination.current = 1;
refreshTable();
};
searchForm.keyword = ''
pagination.current = 1
refreshTable()
}
// 刷新按钮
const handleRefresh = () => {
refreshTable();
message.success("刷新成功");
};
refreshTable()
message.success('刷新成功')
}
// 刷新间隔变化
const handleRefreshIntervalChange = (value) => {
clearRefreshTimer();
clearRefreshTimer()
if (value > 0) {
startRefreshTimer(value);
startRefreshTimer(value)
}
};
}
// 启动刷新定时器
const startRefreshTimer = (interval) => {
refreshTimer = setInterval(() => {
refreshTable();
}, interval);
};
refreshTable()
}, interval)
}
// 清除刷新定时器
const clearRefreshTimer = () => {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
clearInterval(refreshTimer)
refreshTimer = null
}
};
}
// 查看用户会话
const handleViewSessions = (record) => {
dialog.sessions = true;
dialog.sessions = true
setTimeout(() => {
sessionsDialogRef.value?.open().setData(record);
}, 0);
};
sessionsDialogRef.value?.open().setData(record)
}, 0)
}
// 强制用户下线(单个)
const handleOffline = async (record) => {
try {
const res = await authApi.onlineUsers.offline.post(record.id, {});
const res = await authApi.onlineUser.offline.post(record.id, {})
if (res.code === 200) {
message.success("强制下线成功");
refreshTable();
message.success('强制下线成功')
refreshTable()
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("强制下线失败:", error);
message.error("操作失败");
console.error('强制下线失败:', error)
message.error('操作失败')
}
};
}
// 强制用户所有设备下线
const handleOfflineAll = async (record) => {
try {
const res = await authApi.onlineUsers.offlineAll.post(record.id);
const res = await authApi.onlineUser.offlineAll.post(record.id)
if (res.code === 200) {
message.success("全部下线成功");
refreshTable();
message.success('全部下线成功')
refreshTable()
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("全部下线失败:", error);
message.error("操作失败");
console.error('全部下线失败:', error)
message.error('操作失败')
}
};
}
// 全部下线
const handleRefreshAllOffline = () => {
Modal.confirm({
title: "确认操作",
content: "确定要强制所有在线用户下线吗?",
okText: "确定",
cancelText: "取消",
okType: "danger",
title: '确认操作',
content: '确定要强制所有在线用户下线吗?',
okText: '确定',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
try {
// 这里需要遍历所有在线用户并下线
const onlineUsers = tableData.value.filter(
(user) => user.is_online,
);
const onlineUsers = tableData.value.filter((user) => user.is_online)
for (const user of onlineUsers) {
await authApi.onlineUsers.offlineAll.post(user.id);
await authApi.onlineUser.offlineAll.post(user.id)
}
message.success("全部下线成功");
refreshTable();
message.success('全部下线成功')
refreshTable()
} catch (error) {
console.error("全部下线失败:", error);
message.error("操作失败");
console.error('全部下线失败:', error)
message.error('操作失败')
}
},
});
};
})
}
// 会话操作成功回调
const handleSessionsSuccess = () => {
refreshTable();
};
refreshTable()
}
// 格式化日期
const formatDate = (date) => {
if (!date) return "-";
const d = new Date(date);
return d.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
if (!date) return '-'
const d = new Date(date)
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
// 初始化
onMounted(() => {
refreshTable();
refreshTable()
// 启动自动刷新
if (refreshInterval.value > 0) {
startRefreshTimer(refreshInterval.value);
startRefreshTimer(refreshInterval.value)
}
});
})
// 组件卸载时清除定时器
onUnmounted(() => {
clearRefreshTimer();
});
clearRefreshTimer()
})
</script>
<style scoped lang="scss">
@@ -1,35 +1,18 @@
<template>
<a-modal
title="用户会话详情"
:open="visible"
:width="800"
:destroy-on-close="true"
:footer="null"
@cancel="handleCancel"
>
<a-modal title="用户会话详情" :open="visible" :width="800" :destroy-on-close="true" :footer="null" @cancel="handleCancel">
<div class="sessions-content">
<!-- 用户信息 -->
<div class="user-info">
<a-descriptions :column="3" bordered size="small">
<a-descriptions-item label="用户名">{{
userInfo.username
}}</a-descriptions-item>
<a-descriptions-item label="真实姓名">{{
userInfo.real_name
}}</a-descriptions-item>
<a-descriptions-item label="用户名">{{ userInfo.username }}</a-descriptions-item>
<a-descriptions-item label="真实姓名">{{ userInfo.real_name }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag
:color="userInfo.is_online ? 'success' : 'default'"
>
{{ userInfo.is_online ? "在线" : "离线" }}
<a-tag :color="userInfo.is_online ? 'success' : 'default'">
{{ userInfo.is_online ? '在线' : '离线' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="邮箱" :span="2">{{
userInfo.email
}}</a-descriptions-item>
<a-descriptions-item label="手机号">{{
userInfo.phone
}}</a-descriptions-item>
<a-descriptions-item label="邮箱" :span="2">{{ userInfo.email }}</a-descriptions-item>
<a-descriptions-item label="手机号">{{ userInfo.phone }}</a-descriptions-item>
</a-descriptions>
</div>
@@ -37,93 +20,42 @@
<div class="sessions-list">
<div class="list-header">
<span>会话列表{{ sessions.length }} </span>
<a-button
type="link"
size="small"
danger
@click="handleOfflineAll"
>
全部下线
</a-button>
<a-button type="link" size="small" danger @click="handleOfflineAll"> 全部下线 </a-button>
</div>
<a-list :data-source="sessions" :loading="loading" size="small">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #avatar>
<a-avatar
shape="square"
:icon="
item.device_type === 'pc'
? 'DesktopOutlined'
: 'MobileOutlined'
"
/>
<a-avatar shape="square" :icon="item.device_type === 'pc' ? 'DesktopOutlined' : 'MobileOutlined'" />
</template>
<template #title>
<div class="session-title">
<span>{{
getDeviceName(item.device_type)
}}</span>
<a-tag
:color="
item.is_online
? 'success'
: 'default'
"
size="small"
>
{{
item.is_online ? "活跃" : "过期"
}}
<span>{{ getDeviceName(item.device_type) }}</span>
<a-tag :color="item.is_online ? 'success' : 'default'" size="small">
{{ item.is_online ? '活跃' : '过期' }}
</a-tag>
</div>
</template>
<template #description>
<div class="session-info">
<div>
<span class="label">IP地址</span
>{{ item.ip_address }}
</div>
<div>
<span class="label">登录时间</span
>{{ formatDate(item.created_at) }}
</div>
<div>
<span class="label">最后活跃</span
>{{
formatDate(item.last_active_at)
}}
</div>
<div v-if="item.user_agent">
<span class="label">浏览器</span
>{{ item.user_agent }}
</div>
<div><span class="label">IP地址</span>{{ item.ip_address }}</div>
<div><span class="label">登录时间</span>{{ formatDate(item.created_at) }}</div>
<div><span class="label">最后活跃</span>{{ formatDate(item.last_active_at) }}</div>
<div v-if="item.user_agent"><span class="label">浏览器</span>{{ item.user_agent }}</div>
</div>
</template>
</a-list-item-meta>
<template #actions>
<a-popconfirm
v-if="item.is_online"
title="确定强制该会话下线吗?"
@confirm="handleOffline(item)"
>
<a-button type="link" size="small" danger>
强制下线
</a-button>
<a-popconfirm v-if="item.is_online" title="确定强制该会话下线吗?" @confirm="handleOffline(item)">
<a-button type="link" size="small" danger> 强制下线 </a-button>
</a-popconfirm>
<a-tag v-else color="default" size="small"
>已过期</a-tag
>
<a-tag v-else color="default" size="small">已过期</a-tag>
</template>
</a-list-item>
</template>
</a-list>
<a-empty
v-if="!loading && sessions.length === 0"
description="暂无会话数据"
:image-size="80"
/>
<a-empty v-if="!loading && sessions.length === 0" description="暂无会话数据" :image-size="80" />
</div>
</div>
<template #footer>
@@ -133,152 +65,152 @@
</template>
<script setup>
import { ref, reactive } from "vue";
import { message } from "ant-design-vue";
import authApi from "@/api/auth";
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
defineOptions({
name: "OnlineUserSessions",
});
name: 'OnlineUserSessions',
})
const emit = defineEmits(["success", "closed"]);
const emit = defineEmits(['success', 'closed'])
const visible = ref(false);
const loading = ref(false);
const visible = ref(false)
const loading = ref(false)
// 用户信息
const userInfo = reactive({
id: "",
username: "",
real_name: "",
email: "",
phone: "",
id: '',
username: '',
real_name: '',
email: '',
phone: '',
is_online: false,
});
})
// 会话列表
const sessions = ref([]);
const sessions = ref([])
// 打开对话框
const open = () => {
visible.value = true;
visible.value = true
return {
open,
setData,
close,
};
};
}
}
// 关闭对话框
const close = () => {
visible.value = false;
};
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit("closed");
visible.value = false;
};
emit('closed')
visible.value = false
}
// 加载用户会话
const loadUserSessions = async (userId) => {
try {
loading.value = true;
const res = await authApi.onlineUsers.sessions.get(userId);
loading.value = false;
loading.value = true
const res = await authApi.onlineUser.sessions.get(userId)
loading.value = false
if (res.code === 200) {
sessions.value = res.data || [];
sessions.value = res.data || []
}
} catch (error) {
console.error("加载用户会话失败:", error);
loading.value = false;
message.error("加载会话失败");
console.error('加载用户会话失败:', error)
loading.value = false
message.error('加载会话失败')
}
};
}
// 设置数据
const setData = (data) => {
userInfo.id = data.id;
userInfo.username = data.username;
userInfo.real_name = data.real_name;
userInfo.email = data.email;
userInfo.phone = data.phone;
userInfo.is_online = data.is_online;
userInfo.id = data.id
userInfo.username = data.username
userInfo.real_name = data.real_name
userInfo.email = data.email
userInfo.phone = data.phone
userInfo.is_online = data.is_online
// 加载会话列表
loadUserSessions(data.id);
};
loadUserSessions(data.id)
}
// 强制会话下线
const handleOffline = async (session) => {
try {
const res = await authApi.onlineUsers.offline.post(userInfo.id, {
const res = await authApi.onlineUser.offline.post(userInfo.id, {
token: session.token,
});
})
if (res.code === 200) {
message.success("强制下线成功");
emit("success");
message.success('强制下线成功')
emit('success')
// 重新加载会话列表
loadUserSessions(userInfo.id);
loadUserSessions(userInfo.id)
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("强制下线失败:", error);
message.error("操作失败");
console.error('强制下线失败:', error)
message.error('操作失败')
}
};
}
// 全部下线
const handleOfflineAll = async () => {
try {
const res = await authApi.onlineUsers.offlineAll.post(userInfo.id);
const res = await authApi.onlineUser.offlineAll.post(userInfo.id)
if (res.code === 200) {
message.success("全部下线成功");
emit("success");
message.success('全部下线成功')
emit('success')
// 重新加载会话列表
loadUserSessions(userInfo.id);
loadUserSessions(userInfo.id)
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("全部下线失败:", error);
message.error("操作失败");
console.error('全部下线失败:', error)
message.error('操作失败')
}
};
}
// 获取设备名称
const getDeviceName = (deviceType) => {
const deviceMap = {
pc: "电脑端",
mobile: "手机端",
tablet: "平板端",
unknown: "未知设备",
};
return deviceMap[deviceType] || deviceMap.unknown;
};
pc: '电脑端',
mobile: '手机端',
tablet: '平板端',
unknown: '未知设备',
}
return deviceMap[deviceType] || deviceMap.unknown
}
// 格式化日期
const formatDate = (date) => {
if (!date) return "-";
const d = new Date(date);
return d.toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
if (!date) return '-'
const d = new Date(date)
return d.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
// 暴露方法给父组件
defineExpose({
open,
setData,
close,
});
})
</script>
<style scoped lang="scss">
@@ -1,37 +1,15 @@
<template>
<a-form
:model="form"
:rules="rules"
ref="formRef"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
>
<a-form :model="form" :rules="rules" ref="formRef" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<!-- 第一行权限名称和类型 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="权限名称" name="title" required>
<a-input
v-model:value="form.title"
placeholder="如:用户管理"
allow-clear
maxlength="50"
show-count
/>
<a-input v-model:value="form.title" placeholder="如:用户管理" allow-clear maxlength="50" show-count />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="权限类型"
name="type"
required
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-radio-group
v-model:value="form.type"
button-style="solid"
@change="handleTypeChange"
>
<a-form-item label="权限类型" name="type" required :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-radio-group v-model:value="form.type" button-style="solid" @change="handleTypeChange">
<a-radio-button value="menu">菜单</a-radio-button>
<a-radio-button value="api">接口</a-radio-button>
<a-radio-button value="button">按钮</a-radio-button>
@@ -45,29 +23,13 @@
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="上级权限" name="parent_id">
<a-tree-select
v-model:value="form.parent_id"
:tree-data="menuOptions"
:field-names="menuFieldNames"
:tree-default-expand-all="false"
show-icon
placeholder="顶级权限"
allow-clear
tree-node-filter-prop="title"
:disabled="!!menuId"
/>
<a-tree-select v-model:value="form.parent_id" :tree-data="menuOptions" :field-names="menuFieldNames" :tree-default-expand-all="false" show-icon placeholder="顶级权限" allow-clear tree-node-filter-prop="title" :disabled="!!menuId" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="权限编码" name="name" required>
<a-input
v-model:value="form.name"
placeholder="如:system.users.index"
allow-clear
/>
<div class="form-tip">
格式模块.功能.操作系统唯一标识用于权限验证
</div>
<a-input v-model:value="form.name" placeholder="如:system.users.index" allow-clear />
<div class="form-tip">格式模块.功能.操作系统唯一标识用于权限验证</div>
</a-form-item>
</a-col>
</a-row>
@@ -75,38 +37,18 @@
<!-- 第三行路由地址和组件路径菜单类型才显示 -->
<a-row v-if="form.type === 'menu'" :gutter="16">
<a-col :span="12">
<a-form-item
label="路由地址"
name="path"
:required="isLeafNode"
>
<a-input
v-model:value="form.path"
placeholder="/system/users"
allow-clear
/>
<a-form-item label="路由地址" name="path" :required="isLeafNode">
<a-input v-model:value="form.path" placeholder="/system/users" allow-clear />
<div class="form-tip">前端路由路径 /system/users</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="组件路径"
name="component"
:required="isLeafNode"
>
<a-input
v-model:value="form.component"
placeholder="system/users/index"
allow-clear
>
<a-form-item label="组件路径" name="component" :required="isLeafNode">
<a-input v-model:value="form.component" placeholder="system/users/index" allow-clear>
<template #addonBefore>pages/</template>
</a-input>
<div class="form-tip" v-if="!isLeafNode">
父级菜单或包含子菜单时不需要填写
</div>
<div class="form-tip" v-else>
最后一级菜单必须填写 system/users/index
</div>
<div class="form-tip" v-if="!isLeafNode">父级菜单或包含子菜单时不需要填写</div>
<div class="form-tip" v-else>最后一级菜单必须填写 system/users/index</div>
</a-form-item>
</a-col>
</a-row>
@@ -115,14 +57,8 @@
<a-row v-if="form.type === 'api'" :gutter="16">
<a-col :span="12">
<a-form-item label="API路由" name="path" required>
<a-input
v-model:value="form.path"
placeholder="如:users.index"
allow-clear
/>
<div class="form-tip">
后端 API 路由名称用于接口权限验证
</div>
<a-input v-model:value="form.path" placeholder="如:users.index" allow-clear />
<div class="form-tip">后端 API 路由名称用于接口权限验证</div>
</a-form-item>
</a-col>
</a-row>
@@ -131,11 +67,7 @@
<a-row v-if="form.type === 'url'" :gutter="16">
<a-col :span="12">
<a-form-item label="链接地址" name="path" required>
<a-input
v-model:value="form.path"
placeholder="https://example.com"
allow-clear
/>
<a-input v-model:value="form.path" placeholder="https://example.com" allow-clear />
<div class="form-tip">外部链接地址</div>
</a-form-item>
</a-col>
@@ -145,20 +77,12 @@
<a-row v-if="form.type === 'menu'" :gutter="16">
<a-col :span="12">
<a-form-item label="菜单图标" name="icon">
<sc-icon-picker
v-model:value="form.icon"
placeholder="请选择图标"
/>
<sc-icon-picker v-model:value="form.icon" placeholder="请选择图标" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="排序" name="sort">
<a-input-number
v-model:value="form.sort"
:min="0"
:max="10000"
style="width: 100%"
/>
<a-input-number v-model:value="form.sort" :min="0" :max="10000" style="width: 100%" />
<div class="form-tip">数值越小越靠前</div>
</a-form-item>
</a-col>
@@ -172,36 +96,16 @@
<h4>选项设置</h4>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="显示选项"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-checkbox v-model:checked="form.hidden"
>隐藏菜单</a-checkbox
>
<a-checkbox v-model:checked="form.hiddenBreadcrumb"
>隐藏面包屑</a-checkbox
>
<a-checkbox v-model:checked="form.affix"
>固定标签页</a-checkbox
>
<a-form-item label="显示选项" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-checkbox v-model:checked="form.hidden">隐藏菜单</a-checkbox>
<a-checkbox v-model:checked="form.hiddenBreadcrumb">隐藏面包屑</a-checkbox>
<a-checkbox v-model:checked="form.affix">固定标签页</a-checkbox>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item
label="页面缓存"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-switch
v-model:checked="form.keepAlive"
checked-children="启用"
un-checked-children="禁用"
/>
<div class="form-tip">
启用后页面会被缓存切换回来时保留状态
</div>
<a-form-item label="页面缓存" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-switch v-model:checked="form.keepAlive" checked-children="启用" un-checked-children="禁用" />
<div class="form-tip">启用后页面会被缓存切换回来时保留状态</div>
</a-form-item>
</a-col>
</a-row>
@@ -228,34 +132,17 @@
<!-- 状态 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="启用状态"
name="status"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-switch
v-model:checked="statusChecked"
checked-children="启用"
un-checked-children="禁用"
/>
<a-form-item label="启用状态" name="status" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-switch v-model:checked="statusChecked" checked-children="启用" un-checked-children="禁用" />
<div class="form-tip">禁用后该权限将不生效</div>
</a-form-item>
</a-col>
</a-row>
<!-- 操作按钮 -->
<a-form-item
:wrapper-col="{ span: 18, offset: 5 }"
style="margin-top: 32px"
>
<a-form-item :wrapper-col="{ span: 18, offset: 5 }" style="margin-top: 32px">
<a-space>
<a-button
type="primary"
@click="handleSave"
:loading="loading"
size="large"
>
<a-button type="primary" @click="handleSave" :loading="loading" size="large">
<template #icon><CheckOutlined /></template>
保存
</a-button>
@@ -269,159 +156,152 @@
</template>
<script setup>
import { ref, reactive, watch, computed, onMounted } from "vue";
import { message } from "ant-design-vue";
import { CheckOutlined, CloseOutlined } from "@ant-design/icons-vue";
import authApi from "@/api/auth";
import scIconPicker from "@/components/scIconPicker/index.vue";
import { ref, reactive, watch, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue'
import authApi from '@/api/auth'
import scIconPicker from '@/components/scIconPicker/index.vue'
defineOptions({
name: "PermissionSaveForm",
});
name: 'PermissionSaveForm',
})
const props = defineProps({
menu: { type: [Object, Array], default: () => [] },
menuId: { type: [Number, String], default: null },
parentId: { type: [Number, String], default: null },
});
})
const emit = defineEmits(["success", "cancel"]);
const emit = defineEmits(['success', 'cancel'])
// 表单数据
const form = reactive({
id: "",
id: '',
parent_id: 0,
name: "",
title: "",
path: "",
component: "",
icon: "",
name: '',
title: '',
path: '',
component: '',
icon: '',
sort: 0,
type: "menu",
type: 'menu',
status: 1,
target: "_self",
target: '_self',
// meta 字段内容
hidden: false,
hiddenBreadcrumb: false,
keepAlive: false,
affix: false,
});
})
// 表单引用
const formRef = ref();
const loading = ref(false);
const formRef = ref()
const loading = ref(false)
// 验证规则
const rules = {
title: [
{ required: true, message: "请输入权限名称", trigger: "blur" },
{ max: 50, message: "权限名称不能超过50个字符", trigger: "blur" },
{ required: true, message: '请输入权限名称', trigger: 'blur' },
{ max: 50, message: '权限名称不能超过50个字符', trigger: 'blur' },
],
name: [
{ required: true, message: "请输入权限编码", trigger: "blur" },
{ required: true, message: '请输入权限编码', trigger: 'blur' },
{
pattern: /^[a-zA-Z][a-zA-Z0-9_.]*$/,
message: "权限编码格式不正确,格式:模块.功能.操作",
trigger: "blur",
message: '权限编码格式不正确,格式:模块.功能.操作',
trigger: 'blur',
},
],
type: [{ required: true, message: "请选择权限类型", trigger: "change" }],
type: [{ required: true, message: '请选择权限类型', trigger: 'change' }],
path: (rule, value) => {
// 根据类型动态验证
if (
form.type === "menu" ||
form.type === "api" ||
form.type === "url"
) {
if (form.type === 'menu' || form.type === 'api' || form.type === 'url') {
if (!value || !value.trim()) {
return Promise.reject("请输入" + getPathLabel(form.type));
return Promise.reject('请输入' + getPathLabel(form.type))
}
}
return Promise.resolve();
return Promise.resolve()
},
component: (rule, value) => {
// 仅在菜单类型且为叶子节点时验证
if (form.type === "menu" && isLeafNode.value) {
if (form.type === 'menu' && isLeafNode.value) {
if (!value || !value.trim()) {
return Promise.reject("请输入组件路径");
return Promise.reject('请输入组件路径')
}
}
return Promise.resolve();
return Promise.resolve()
},
};
}
// 路径字段标签
const getPathLabel = (type) => {
const labelMap = {
menu: "路由地址",
api: "API路由",
url: "链接地址",
};
return labelMap[type] || "路径";
};
menu: '路由地址',
api: 'API路由',
url: '链接地址',
}
return labelMap[type] || '路径'
}
// 状态开关计算属性
const statusChecked = computed({
get: () => form.status === 1,
set: (val) => {
form.status = val ? 1 : 0;
form.status = val ? 1 : 0
},
});
})
// 判断是否为叶子节点(没有子节点的节点)
const isLeafNode = computed(() => {
// 这里需要根据当前节点是否有子节点来判断
// 暂时返回 false,需要根据实际数据判断
return !hasChildren.value;
});
return !hasChildren.value
})
// 判断是否有子节点
const hasChildren = computed(() => {
if (!form.id || !props.menu) return false;
const node = findMenuNode(props.menu, form.id);
return node && node.children && node.children.length > 0;
});
if (!form.id || !props.menu) return false
const node = findMenuNode(props.menu, form.id)
return node && node.children && node.children.length > 0
})
// 菜单选项
const menuOptions = ref([]);
const menuOptions = ref([])
const menuFieldNames = {
value: "id",
label: "title",
children: "children",
};
value: 'id',
label: 'title',
children: 'children',
}
// 筛单化菜单树,排除自己和子节点
const treeToMap = (tree, excludeId = null) => {
const map = [];
const map = []
tree.forEach((item) => {
if (item.id === excludeId) return; // 排除自己
if (item.id === excludeId) return // 排除自己
const obj = {
id: item.id,
parent_id: item.parent_id,
title: item.title,
children:
item.children && item.children.length > 0
? treeToMap(item.children, excludeId)
: null,
};
map.push(obj);
});
return map;
};
children: item.children && item.children.length > 0 ? treeToMap(item.children, excludeId) : null,
}
map.push(obj)
})
return map
}
// 查找权限节点
const findMenuNode = (tree, id) => {
for (const node of tree) {
if (node.id === id) {
return node;
return node
}
if (node.children && node.children.length > 0) {
const found = findMenuNode(node.children, id);
if (found) return found;
const found = findMenuNode(node.children, id)
if (found) return found
}
}
return null;
};
return null
}
// 监听菜单树变化
watch(
@@ -429,85 +309,85 @@ watch(
(newVal) => {
if (newVal) {
// 排除当前编辑的节点,避免选择自己作为父节点
menuOptions.value = treeToMap(newVal, props.menuId);
menuOptions.value = treeToMap(newVal, props.menuId)
}
},
{ deep: true, immediate: true },
);
)
// 监听 menuId 变化,从菜单树中查找并赋值
watch(
() => props.menuId,
(newVal) => {
if (newVal && props.menu && props.menu.length > 0) {
const menuNode = findMenuNode(props.menu, newVal);
const menuNode = findMenuNode(props.menu, newVal)
if (menuNode) {
setData(menuNode, props.parentId);
setData(menuNode, props.parentId)
}
} else if (!newVal) {
// 清空表单
resetForm();
resetForm()
}
},
);
)
// 类型切换处理
const handleTypeChange = () => {
// 类型切换时清空一些字段
form.path = "";
form.component = "";
form.icon = "";
form.path = ''
form.component = ''
form.icon = ''
// 非菜单类型时清空meta相关字段
if (form.type !== "menu") {
form.hidden = false;
form.hiddenBreadcrumb = false;
form.keepAlive = false;
form.affix = false;
if (form.type !== 'menu') {
form.hidden = false
form.hiddenBreadcrumb = false
form.keepAlive = false
form.affix = false
}
// 非链接类型时重置target
if (form.type !== "url") {
form.target = "_self";
if (form.type !== 'url') {
form.target = '_self'
}
};
}
// 重置表单
const resetForm = () => {
Object.assign(form, {
id: "",
id: '',
parent_id: props.parentId || 0,
name: "",
title: "",
path: "",
component: "",
icon: "",
name: '',
title: '',
path: '',
component: '',
icon: '',
sort: 0,
type: "menu",
type: 'menu',
status: 1,
target: "_self",
target: '_self',
hidden: false,
hiddenBreadcrumb: false,
keepAlive: false,
affix: false,
});
};
})
}
// 加载权限详情
const loadMenuDetail = async (id) => {
try {
const res = await authApi.permissions.detail.get(id);
const res = await authApi.permission.detail.get(id)
if (res.code === 200 && res.data) {
setData(res.data, props.parentId);
setData(res.data, props.parentId)
}
} catch (error) {
console.error("加载权限详情失败:", error);
console.error('加载权限详情失败:', error)
}
};
}
// 保存
const handleSave = async () => {
try {
await formRef.value.validate();
loading.value = true;
await formRef.value.validate()
loading.value = true
// 构建提交数据
const submitData = {
@@ -523,104 +403,97 @@ const handleSave = async () => {
status: form.status,
target: form.target,
meta: null,
};
}
// 仅菜单类型才有meta字段
if (form.type === "menu") {
if (form.type === 'menu') {
submitData.meta = {
hidden: form.hidden,
hiddenBreadcrumb: form.hiddenBreadcrumb,
keepAlive: form.keepAlive,
affix: form.affix,
};
}
}
// 根据类型处理空值
if (form.type === "button" || form.type === "api") {
submitData.component = "";
submitData.icon = "";
if (form.type === 'button' || form.type === 'api') {
submitData.component = ''
submitData.icon = ''
}
if (form.type === "button") {
submitData.path = "";
if (form.type === 'button') {
submitData.path = ''
}
if (
form.type === "api" ||
form.type === "button" ||
form.type === "url"
) {
submitData.meta = null;
if (form.type === 'api' || form.type === 'button' || form.type === 'url') {
submitData.meta = null
}
let res = {};
let res = {}
if (form.id) {
res = await authApi.permissions.edit.put(form.id, submitData);
res = await authApi.permission.edit.put(form.id, submitData)
} else {
res = await authApi.permissions.add.post(submitData);
res = await authApi.permission.add.post(submitData)
}
loading.value = false;
loading.value = false
if (res.code === 200) {
message.success("保存成功");
emit("success");
message.success('保存成功')
emit('success')
} else {
message.error(res.message || "保存失败");
message.error(res.message || '保存失败')
}
} catch (error) {
console.error("表单验证失败", error);
loading.value = false;
console.error('表单验证失败', error)
loading.value = false
if (error?.errorFields) {
// 表单验证失败
return;
return
}
message.error("保存失败");
message.error('保存失败')
}
};
}
// 表单注入数据
const setData = (data, pid) => {
form.id = data.id || "";
form.parent_id = data.parent_id !== undefined ? data.parent_id : pid || 0;
form.name = data.name || "";
form.title = data.title || "";
form.path = data.path || "";
form.component = data.component || "";
form.icon = data.icon || "";
form.sort = data.sort || 0;
form.type = data.type || "menu";
form.status = data.status !== undefined ? data.status : 1;
form.target = data.target || "_self";
form.id = data.id || ''
form.parent_id = data.parent_id !== undefined ? data.parent_id : pid || 0
form.name = data.name || ''
form.title = data.title || ''
form.path = data.path || ''
form.component = data.component || ''
form.icon = data.icon || ''
form.sort = data.sort || 0
form.type = data.type || 'menu'
form.status = data.status !== undefined ? data.status : 1
form.target = data.target || '_self'
// 解析 meta 字段
const meta =
data.meta && typeof data.meta === "string"
? JSON.parse(data.meta)
: data.meta || {};
form.hidden = meta.hidden || false;
form.hiddenBreadcrumb = meta.hiddenBreadcrumb || false;
form.keepAlive = meta.keepAlive || false;
form.affix = meta.affix || false;
};
const meta = data.meta && typeof data.meta === 'string' ? JSON.parse(data.meta) : data.meta || {}
form.hidden = meta.hidden || false
form.hiddenBreadcrumb = meta.hiddenBreadcrumb || false
form.keepAlive = meta.keepAlive || false
form.affix = meta.affix || false
}
// 初始化
onMounted(() => {
if (props.menuId) {
loadMenuDetail(props.menuId);
loadMenuDetail(props.menuId)
} else if (props.parentId) {
form.parent_id = props.parentId;
form.parent_id = props.parentId
}
});
})
// 清空表单验证
const clearValidate = () => {
formRef.value?.clearValidate();
};
formRef.value?.clearValidate()
}
// 暴露方法给父组件
defineExpose({
setData,
clearValidate,
resetForm,
});
})
</script>
<style scoped lang="scss">
@@ -3,43 +3,24 @@
<div class="left-box">
<div class="header">
<div class="search-wrapper">
<a-input
v-model:value="menuFilterText"
placeholder="搜索权限名称或编码..."
allow-clear
@change="handleMenuSearch"
>
<a-input v-model:value="menuFilterText" placeholder="搜索权限名称或编码..." allow-clear @change="handleMenuSearch">
<template #prefix>
<SearchOutlined
style="color: rgba(0, 0, 0, 0.45)"
/>
<SearchOutlined style="color: rgba(0, 0, 0, 0.45)" />
</template>
</a-input>
</div>
<div class="actions">
<a-space size="small">
<a-tooltip
:title="isAllExpanded ? '折叠全部' : '展开全部'"
>
<a-button
type="text"
size="small"
@click="handleToggleExpand"
>
<a-tooltip :title="isAllExpanded ? '折叠全部' : '展开全部'">
<a-button type="text" size="small" @click="handleToggleExpand">
<template #icon>
<UnorderedListOutlined
v-if="!isAllExpanded"
/>
<UnorderedListOutlined v-if="!isAllExpanded" />
<OrderedListOutlined v-else />
</template>
</a-button>
</a-tooltip>
<a-tooltip title="添加根权限">
<a-button
type="text"
size="small"
@click="handleAdd(null)"
>
<a-button type="text" size="small" @click="handleAdd(null)">
<template #icon><PlusOutlined /></template>
</a-button>
</a-tooltip>
@@ -67,40 +48,19 @@
@check="onMenuCheck"
>
<template #icon="{ dataRef }">
<FolderOutlined
v-if="
dataRef.type === 'menu' &&
dataRef.children?.length
"
/>
<FolderOpenOutlined
v-else-if="dataRef.type === 'menu'"
/>
<FolderOutlined v-if="dataRef.type === 'menu' && dataRef.children?.length" />
<FolderOpenOutlined v-else-if="dataRef.type === 'menu'" />
<ApiOutlined v-else-if="dataRef.type === 'api'" />
<ControlOutlined v-else />
</template>
<template #title="{ dataRef }">
<span class="tree-node-content">
<span class="tree-node-title">{{
dataRef.title
}}</span>
<a-tag
v-if="dataRef.name"
class="tree-node-code"
size="small"
>{{ dataRef.name }}</a-tag
>
<a-tag
v-if="dataRef.type !== 'menu'"
:color="getTypeColor(dataRef.type)"
size="small"
>
<span class="tree-node-title">{{ dataRef.title }}</span>
<a-tag v-if="dataRef.name" class="tree-node-code" size="small">{{ dataRef.name }}</a-tag>
<a-tag v-if="dataRef.type !== 'menu'" :color="getTypeColor(dataRef.type)" size="small">
{{ getTypeLabel(dataRef.type) }}
</a-tag>
<span
v-if="!dataRef.status"
class="tree-node-disabled"
>
<span v-if="!dataRef.status" class="tree-node-disabled">
<StopOutlined />
</span>
</span>
@@ -112,24 +72,13 @@
<div class="right-box">
<div class="header">
<div class="title-wrapper">
<span class="title">{{
selectedMenu?.title || "请选择权限节点"
}}</span>
<a-tag
v-if="selectedMenu"
:color="getTypeColor(selectedMenu.type)"
size="small"
>
<span class="title">{{ selectedMenu?.title || '请选择权限节点' }}</span>
<a-tag v-if="selectedMenu" :color="getTypeColor(selectedMenu.type)" size="small">
{{ getTypeLabel(selectedMenu.type) }}
</a-tag>
</div>
<a-space>
<a-button
v-if="checkedMenuKeys.length > 0"
danger
size="small"
@click="handleDeleteBatch"
>
<a-button v-if="checkedMenuKeys.length > 0" danger size="small" @click="handleDeleteBatch">
<template #icon><DeleteOutlined /></template>
批量删除 ({{ checkedMenuKeys.length }})
</a-button>
@@ -140,15 +89,9 @@
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleImport">
<ImportOutlined />导入权限
</a-menu-item>
<a-menu-item @click="handleExport">
<ExportOutlined />导出权限
</a-menu-item>
<a-menu-item @click="handleDownloadTemplate">
<DownloadOutlined />下载模板
</a-menu-item>
<a-menu-item @click="handleImport"> <ImportOutlined />导入权限 </a-menu-item>
<a-menu-item @click="handleExport"> <ExportOutlined />导出权限 </a-menu-item>
<a-menu-item @click="handleDownloadTemplate"> <DownloadOutlined />下载模板 </a-menu-item>
</a-menu>
</template>
</a-dropdown>
@@ -160,411 +103,355 @@
</div>
<div class="body">
<a-spin :spinning="detailLoading" :delay="200">
<save-form
v-if="selectedMenu"
:menu="menuTree"
:menu-id="selectedMenu.id"
:parent-id="parentId"
@success="handleSaveSuccess"
/>
<a-empty
v-else
description="请选择左侧权限节点后操作"
:image-size="100"
/>
<save-form v-if="selectedMenu" :menu="menuTree" :menu-id="selectedMenu.id" :parent-id="parentId" @success="handleSaveSuccess" />
<a-empty v-else description="请选择左侧权限节点后操作" :image-size="100" />
</a-spin>
</div>
</div>
</div>
<!-- 导入权限弹窗 -->
<sc-import
v-model:open="dialog.import"
title="导入权限"
:api="authApi.permissions.import.post"
:template-api="authApi.permissions.downloadTemplate.get"
filename="权限"
@success="handleImportSuccess"
/>
<sc-import v-model:open="dialog.import" title="导入权限" :api="authApi.permission.import.post" :template-api="authApi.permission.downloadTemplate.get" filename="权限" @success="handleImportSuccess" />
<!-- 导出权限弹窗 -->
<sc-export
v-model:open="dialog.export"
title="导出权限"
:api="handleExportApi"
:default-filename="`权限列表_${Date.now()}`"
:show-options="false"
tip="导出当前选中或所有权限数据"
@success="handleExportSuccess"
/>
<sc-export v-model:open="dialog.export" title="导出权限" :api="handleExportApi" :default-filename="`权限列表_${Date.now()}`" :show-options="false" tip="导出当前选中或所有权限数据" @success="handleExportSuccess" />
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
import { message, Modal } from "ant-design-vue";
import {
SearchOutlined,
ReloadOutlined,
PlusOutlined,
FolderOutlined,
FolderOpenOutlined,
ApiOutlined,
ControlOutlined,
UnorderedListOutlined,
OrderedListOutlined,
DeleteOutlined,
StopOutlined,
ImportOutlined,
ExportOutlined,
DownloadOutlined,
MoreOutlined,
} from "@ant-design/icons-vue";
import { computed } from "vue";
import saveForm from "./components/SaveForm.vue";
import scImport from "@/components/scImport/index.vue";
import scExport from "@/components/scExport/index.vue";
import authApi from "@/api/auth";
import { ref, onMounted, nextTick } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { SearchOutlined, ReloadOutlined, PlusOutlined, FolderOutlined, FolderOpenOutlined, ApiOutlined, ControlOutlined, UnorderedListOutlined, OrderedListOutlined, DeleteOutlined, StopOutlined, ImportOutlined, ExportOutlined, DownloadOutlined, MoreOutlined } from '@ant-design/icons-vue'
import { computed } from 'vue'
import saveForm from './components/SaveForm.vue'
import scImport from '@/components/scImport/index.vue'
import scExport from '@/components/scExport/index.vue'
import authApi from '@/api/auth'
defineOptions({
name: "authPermission",
});
name: 'authPermission',
})
// 菜单树数据
const menuTree = ref([]);
const filteredMenuTree = ref([]);
const selectedMenuKeys = ref([]);
const checkedMenuKeys = ref([]);
const expandedKeys = ref([]);
const menuFilterText = ref("");
const menuTree = ref([])
const filteredMenuTree = ref([])
const selectedMenuKeys = ref([])
const checkedMenuKeys = ref([])
const expandedKeys = ref([])
const menuFilterText = ref('')
// 当前选中的菜单
const selectedMenu = ref(null);
const parentId = ref(null);
const selectedMenu = ref(null)
const parentId = ref(null)
// 加载状态
const loading = ref(false);
const detailLoading = ref(false);
const loading = ref(false)
const detailLoading = ref(false)
// 对话框状态
const dialog = ref({
import: false,
export: false,
});
})
// 树引用
const treeRef = ref();
const treeRef = ref()
// 是否全部展开
const isAllExpanded = computed(() => {
const allKeys = getAllKeys(filteredMenuTree.value);
return allKeys.length > 0 && expandedKeys.value.length === allKeys.length;
});
const allKeys = getAllKeys(filteredMenuTree.value)
return allKeys.length > 0 && expandedKeys.value.length === allKeys.length
})
// 切换展开/折叠
const handleToggleExpand = () => {
if (isAllExpanded.value) {
handleCollapseAll();
handleCollapseAll()
} else {
handleExpandAll();
handleExpandAll()
}
};
}
// 加载权限树
const loadMenuTree = async () => {
try {
loading.value = true;
const res = await authApi.permissions.tree.get();
loading.value = true
const res = await authApi.permission.tree.get()
if (res.code === 200) {
menuTree.value = res.data || [];
filteredMenuTree.value = res.data || [];
menuTree.value = res.data || []
filteredMenuTree.value = res.data || []
// 默认展开第一层
expandAllKeys(menuTree.value, 1);
expandAllKeys(menuTree.value, 1)
} else {
message.error(res.message || "加载权限树失败");
message.error(res.message || '加载权限树失败')
}
} catch (error) {
console.error("加载权限树失败:", error);
message.error("加载权限树失败");
console.error('加载权限树失败:', error)
message.error('加载权限树失败')
} finally {
loading.value = false;
loading.value = false
}
};
}
// 刷新
const handleRefresh = () => {
loadMenuTree();
loadMenuTree()
if (selectedMenu.value) {
// 重新获取当前选中的权限详情
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id);
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id)
if (menuNode) {
selectedMenu.value = menuNode;
selectedMenu.value = menuNode
}
}
};
}
// 搜索权限
const handleMenuSearch = (e) => {
const keyword = (e.target?.value || "").trim();
menuFilterText.value = keyword;
const keyword = (e.target?.value || '').trim()
menuFilterText.value = keyword
if (!keyword) {
filteredMenuTree.value = menuTree.value;
return;
filteredMenuTree.value = menuTree.value
return
}
// 递归过滤权限树(支持搜索名称和编码)
const filterTree = (nodes) => {
return nodes.reduce((acc, node) => {
const titleMatch =
node.title &&
node.title.toLowerCase().includes(keyword.toLowerCase());
const nameMatch =
node.name &&
node.name.toLowerCase().includes(keyword.toLowerCase());
const isMatch = titleMatch || nameMatch;
const filteredChildren = node.children
? filterTree(node.children)
: [];
const titleMatch = node.title && node.title.toLowerCase().includes(keyword.toLowerCase())
const nameMatch = node.name && node.name.toLowerCase().includes(keyword.toLowerCase())
const isMatch = titleMatch || nameMatch
const filteredChildren = node.children ? filterTree(node.children) : []
if (isMatch || filteredChildren.length > 0) {
acc.push({
...node,
children:
filteredChildren.length > 0
? filteredChildren
: undefined,
});
children: filteredChildren.length > 0 ? filteredChildren : undefined,
})
}
return acc;
}, []);
};
return acc
}, [])
}
filteredMenuTree.value = filterTree(menuTree.value);
filteredMenuTree.value = filterTree(menuTree.value)
// 搜索时展开所有匹配节点
expandAllKeys(filteredMenuTree.value);
};
expandAllKeys(filteredMenuTree.value)
}
// 查找权限节点
const findMenuNode = (tree, id) => {
for (const node of tree) {
if (node.id === id) {
return node;
return node
}
if (node.children && node.children.length > 0) {
const found = findMenuNode(node.children, id);
if (found) return found;
const found = findMenuNode(node.children, id)
if (found) return found
}
}
return null;
};
return null
}
// 查找父节点ID
const findParentId = (tree, id) => {
for (const node of tree) {
if (node.children && node.children.length > 0) {
const child = node.children.find((child) => child.id === id);
const child = node.children.find((child) => child.id === id)
if (child) {
return node.id;
return node.id
}
const found = findParentId(node.children, id);
if (found !== null) return found;
const found = findParentId(node.children, id)
if (found !== null) return found
}
}
return null;
};
return null
}
// 限制选择事件
const onMenuSelect = (selectedKeys, { selected }) => {
if (selected) {
const menuId = selectedKeys[0];
const menuNode = findMenuNode(menuTree.value, menuId);
selectedMenu.value = menuNode;
parentId.value = findParentId(menuTree.value, menuId);
const menuId = selectedKeys[0]
const menuNode = findMenuNode(menuTree.value, menuId)
selectedMenu.value = menuNode
parentId.value = findParentId(menuTree.value, menuId)
} else {
selectedMenu.value = null;
parentId.value = null;
selectedMenu.value = null
parentId.value = null
}
};
}
// 限制勾选事件
const onMenuCheck = (checkedKeys, info) => {
console.log("checkedKeys:", checkedKeys, "info:", info);
};
console.log('checkedKeys:', checkedKeys, 'info:', info)
}
// 获取所有节点ID(用于展开/折叠)
const getAllKeys = (nodes) => {
const keys = [];
const keys = []
const traverse = (items) => {
items.forEach((item) => {
keys.push(item.id);
keys.push(item.id)
if (item.children?.length) {
traverse(item.children);
traverse(item.children)
}
});
};
traverse(nodes);
return keys;
};
})
}
traverse(nodes)
return keys
}
// 展开全部
const handleExpandAll = () => {
expandedKeys.value = getAllKeys(filteredMenuTree.value);
};
expandedKeys.value = getAllKeys(filteredMenuTree.value)
}
// 折叠全部
const handleCollapseAll = () => {
expandedKeys.value = [];
};
expandedKeys.value = []
}
// 自动展开指定层级的节点
const expandAllKeys = (nodes, maxLevel = 3) => {
const keys = [];
const keys = []
const traverse = (items, level = 1) => {
items.forEach((item) => {
if (level < maxLevel && item.children?.length) {
keys.push(item.id);
traverse(item.children, level + 1);
keys.push(item.id)
traverse(item.children, level + 1)
}
});
};
traverse(nodes);
expandedKeys.value = keys;
};
})
}
traverse(nodes)
expandedKeys.value = keys
}
// 获取权限类型标签
const getTypeLabel = (type) => {
const typeMap = {
menu: "菜单",
api: "接口",
button: "按钮",
url: "链接",
};
return typeMap[type] || type;
};
menu: '菜单',
api: '接口',
button: '按钮',
url: '链接',
}
return typeMap[type] || type
}
// 获取权限类型颜色
const getTypeColor = (type) => {
const colorMap = {
menu: "blue",
api: "green",
button: "orange",
url: "purple",
};
return colorMap[type] || "default";
};
menu: 'blue',
api: 'green',
button: 'orange',
url: 'purple',
}
return colorMap[type] || 'default'
}
// 批量删除权限
const handleDeleteBatch = async () => {
if (checkedMenuKeys.value.length === 0) {
message.warning("请选择需要删除的权限");
return;
message.warning('请选择需要删除的权限')
return
}
Modal.confirm({
title: "确认删除",
title: '确认删除',
content: `确定删除已选择的 ${checkedMenuKeys.value.length} 个权限吗?`,
okText: "删除",
okType: "danger",
cancelText: "取消",
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const res = await authApi.permissions.batchDelete.post({
const res = await authApi.permission.batchDelete.post({
ids: checkedMenuKeys.value,
});
})
if (res.code === 200) {
message.success("删除成功");
message.success('删除成功')
// 如果当前选中的权限被删除了,清空选择
if (
selectedMenu.value &&
checkedMenuKeys.value.includes(selectedMenu.value.id)
) {
selectedMenu.value = null;
selectedMenuKeys.value = [];
if (selectedMenu.value && checkedMenuKeys.value.includes(selectedMenu.value.id)) {
selectedMenu.value = null
selectedMenuKeys.value = []
}
checkedMenuKeys.value = [];
await loadMenuTree();
checkedMenuKeys.value = []
await loadMenuTree()
} else {
message.error(res.message || "删除失败");
message.error(res.message || '删除失败')
}
} catch (error) {
console.error("删除权限失败:", error);
message.error("删除失败");
console.error('删除权限失败:', error)
message.error('删除失败')
}
},
});
};
})
}
// 保存成功回调
const handleSaveSuccess = async () => {
await loadMenuTree();
await loadMenuTree()
// 重新设置当前选中的权限
if (selectedMenu.value) {
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id);
selectedMenu.value = menuNode;
const menuNode = findMenuNode(menuTree.value, selectedMenu.value.id)
selectedMenu.value = menuNode
}
message.success("保存成功");
};
message.success('保存成功')
}
// 导出权限
const handleExport = () => {
dialog.value.export = true;
};
dialog.value.export = true
}
// 导出API封装
const handleExportApi = async () => {
return await authApi.permissions.export.post({
ids:
checkedMenuKeys.value.length > 0
? checkedMenuKeys.value
: undefined,
});
};
return await authApi.permission.export.post({
ids: checkedMenuKeys.value.length > 0 ? checkedMenuKeys.value : undefined,
})
}
// 导出成功回调
const handleExportSuccess = () => {
checkedMenuKeys.value = [];
};
checkedMenuKeys.value = []
}
// 导入权限
const handleImport = () => {
dialog.value.import = true;
};
dialog.value.import = true
}
// 导入成功回调
const handleImportSuccess = () => {
loadMenuTree();
};
loadMenuTree()
}
// 下载模板
const handleDownloadTemplate = async () => {
try {
const blob = await authApi.permissions.downloadTemplate.get();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "权限导入模板.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success("下载成功");
const blob = await authApi.permission.downloadTemplate.get()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '权限导入模板.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('下载成功')
} catch (error) {
console.error("下载模板失败:", error);
message.error("下载失败");
console.error('下载模板失败:', error)
message.error('下载失败')
}
};
}
// 初始化
onMounted(() => {
loadMenuTree();
});
loadMenuTree()
})
defineExpose({
loadMenuTree,
handleExpandAll,
handleCollapseAll,
});
})
</script>
<style scoped lang="scss">
@@ -640,7 +527,7 @@ defineExpose({
}
.tree-node-code {
font-family: "Consolas", "Monaco", monospace;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
background: #f0f0f0;
border: none;
@@ -1,151 +1,129 @@
<template>
<a-modal
:title="title"
:open="visible"
:width="500"
:destroy-on-close="true"
@cancel="handleCancel"
>
<a-form
:model="form"
:rules="rules"
ref="dialogForm"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
>
<a-modal :title="title" :open="visible" :width="500" :destroy-on-close="true" @cancel="handleCancel">
<a-form :model="form" :rules="rules" ref="dialogForm" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-form-item label="角色名称" name="name">
<a-input
v-model:value="form.name"
placeholder="请输入新角色名称"
allow-clear
/>
<a-input v-model:value="form.name" placeholder="请输入新角色名称" allow-clear />
</a-form-item>
<a-form-item label="角色编码" name="code">
<a-input
v-model:value="form.code"
placeholder="请输入新角色编码"
allow-clear
/>
<a-input v-model:value="form.code" placeholder="请输入新角色编码" allow-clear />
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="handleCancel"> </a-button>
<a-button type="primary" :loading="loading" @click="handleOk"
> </a-button
>
<a-button type="primary" :loading="loading" @click="handleOk"> </a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { message } from "ant-design-vue";
import authApi from "@/api/auth";
import { ref, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const emit = defineEmits(["success", "closed"]);
const emit = defineEmits(['success', 'closed'])
const visible = ref(false);
const loading = ref(false);
const sourceId = ref(null);
const sourceName = ref("");
const sourceCode = ref("");
const visible = ref(false)
const loading = ref(false)
const sourceId = ref(null)
const sourceName = ref('')
const sourceCode = ref('')
// 表单数据
const form = reactive({
name: "",
code: "",
});
name: '',
code: '',
})
// 标题
const title = computed(() => (sourceId.value ? "复制角色" : "批量复制"));
const title = computed(() => (sourceId.value ? '复制角色' : '批量复制'))
// 表单引用
const dialogForm = ref();
const dialogForm = ref()
// 验证规则
const rules = {
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
code: [
{ required: true, message: "请输入角色编码", trigger: "blur" },
{ required: true, message: '请输入角色编码', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: "角色编码只能包含字母、数字和下划线",
trigger: "blur",
message: '角色编码只能包含字母、数字和下划线',
trigger: 'blur',
},
],
};
}
// 打开对话框
const open = (data = null) => {
if (data && data.id) {
// 单个复制
sourceId.value = data.id;
sourceName.value = data.name;
sourceCode.value = data.code;
form.name = `${data.name}_副本`;
form.code = `${data.code}_copy`;
sourceId.value = data.id
sourceName.value = data.name
sourceCode.value = data.code
form.name = `${data.name}_副本`
form.code = `${data.code}_copy`
} else {
// 批量复制(暂不支持自定义名称,直接在后端处理)
sourceId.value = null;
sourceName.value = "";
sourceCode.value = "";
form.name = "";
form.code = "";
sourceId.value = null
sourceName.value = ''
sourceCode.value = ''
form.name = ''
form.code = ''
}
visible.value = true;
visible.value = true
return {
open,
close,
};
};
}
}
// 关闭对话框
const close = () => {
visible.value = false;
};
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit("closed");
visible.value = false;
};
emit('closed')
visible.value = false
}
// 处理确定
const handleOk = async () => {
try {
if (sourceId.value) {
// 单个复制
await dialogForm.value.validate();
loading.value = true;
const res = await authApi.roles.copy.post(sourceId.value, {
await dialogForm.value.validate()
loading.value = true
const res = await authApi.role.copy.post(sourceId.value, {
name: form.name,
code: form.code,
});
loading.value = false;
})
loading.value = false
if (res.code === 200) {
emit("success");
visible.value = false;
message.success("复制成功");
emit('success')
visible.value = false
message.success('复制成功')
} else {
message.error(res.message || "复制失败");
message.error(res.message || '复制失败')
}
} else {
// 批量复制(通过外部传入的 ids)
emit("success");
visible.value = false;
emit('success')
visible.value = false
}
} catch (error) {
console.error("复制失败:", error);
loading.value = false;
console.error('复制失败:', error)
loading.value = false
}
};
}
// 暴露方法给父组件
defineExpose({
open,
close,
});
})
</script>
@@ -1,23 +1,8 @@
<template>
<a-modal
title="角色权限设置"
:open="visible"
:width="600"
:destroy-on-close="true"
@cancel="handleCancel"
>
<a-modal title="角色权限设置" :open="visible" :width="600" :destroy-on-close="true" @cancel="handleCancel">
<div class="permission-content">
<div class="permission-tree">
<a-tree
ref="menuTreeRef"
v-model:checkedKeys="checkedPermissionIds"
:tree-data="permissionTree"
:field-names="fieldNames"
:checkable="true"
:default-expand-all="true"
:check-strictly="false"
:selectable="false"
>
<a-tree ref="menuTreeRef" v-model:checkedKeys="checkedPermissionIds" :tree-data="permissionTree" :field-names="fieldNames" :checkable="true" :default-expand-all="true" :check-strictly="false" :selectable="false">
<template #title="{ title }">
{{ title }}
</template>
@@ -27,131 +12,129 @@
<template #footer>
<a-space>
<a-button @click="handleCancel"> </a-button>
<a-button type="primary" :loading="isSaveing" @click="submit"
> </a-button
>
<a-button type="primary" :loading="isSaveing" @click="submit"> </a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup>
import { ref, reactive } from "vue";
import { message } from "ant-design-vue";
import authApi from "@/api/auth";
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const emit = defineEmits(["success", "closed"]);
const emit = defineEmits(['success', 'closed'])
const visible = ref(false);
const isSaveing = ref(false);
const menuTreeRef = ref();
const visible = ref(false)
const isSaveing = ref(false)
const menuTreeRef = ref()
// 权限树数据
const permissionTree = ref([]);
const checkedPermissionIds = ref([]);
const permissionTree = ref([])
const checkedPermissionIds = ref([])
// 树字段映射
const fieldNames = {
title: "title",
key: "id",
children: "children",
};
title: 'title',
key: 'id',
children: 'children',
}
// 表单数据
const form = reactive({
role_id: "",
role_id: '',
permission_ids: [],
});
})
// 打开对话框
const open = () => {
visible.value = true;
visible.value = true
return {
open,
setData,
close,
};
};
}
}
// 关闭对话框
const close = () => {
visible.value = false;
};
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit("closed");
visible.value = false;
};
emit('closed')
visible.value = false
}
// 提交保存
const submit = async () => {
try {
isSaveing.value = true;
isSaveing.value = true
// 获取选中的权限 ID
form.permission_ids = checkedPermissionIds.value || [];
form.permission_ids = checkedPermissionIds.value || []
const res = await authApi.roles.permissions.post(form.role_id, {
const res = await authApi.role.permissions.post(form.role_id, {
permission_ids: form.permission_ids,
});
})
isSaveing.value = false;
isSaveing.value = false
if (res.code === 200) {
emit("success", form);
visible.value = false;
message.success("操作成功");
emit('success', form)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("保存权限失败:", error);
isSaveing.value = false;
message.error("操作失败");
console.error('保存权限失败:', error)
isSaveing.value = false
message.error('操作失败')
}
};
}
// 获取权限树
const loadPermissionTree = async () => {
try {
const res = await authApi.permissions.tree.get();
permissionTree.value = res.data || [];
const res = await authApi.permission.tree.get()
permissionTree.value = res.data || []
} catch (error) {
console.error("获取权限树失败:", error);
message.error("获取权限树失败");
console.error('获取权限树失败:', error)
message.error('获取权限树失败')
}
};
}
// 获取角色已有权限
const loadRolePermissions = async (roleId) => {
try {
const res = await authApi.roles.permissions.get(roleId);
const res = await authApi.role.permissions.get(roleId)
if (res.code === 200 && res.data) {
checkedPermissionIds.value = res.data.map((item) => item.id);
checkedPermissionIds.value = res.data.map((item) => item.id)
}
} catch (error) {
console.error("获取角色权限失败:", error);
console.error('获取角色权限失败:', error)
}
};
}
// 设置数据
const setData = async (data) => {
form.role_id = data.id;
checkedPermissionIds.value = [];
form.role_id = data.id
checkedPermissionIds.value = []
// 加载角色已有的权限
await loadRolePermissions(data.id);
};
await loadRolePermissions(data.id)
}
// 组件挂载时加载数据
loadPermissionTree();
loadPermissionTree()
// 暴露方法给父组件
defineExpose({
open,
setData,
close,
});
})
</script>
<style scoped>
@@ -1,187 +1,142 @@
<template>
<a-modal
:title="titleMap[mode]"
:open="visible"
:width="500"
:destroy-on-close="true"
@cancel="handleCancel"
>
<a-form
:model="form"
:rules="rules"
:disabled="mode === 'show'"
ref="dialogForm"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
>
<a-modal :title="titleMap[mode]" :open="visible" :width="500" :destroy-on-close="true" @cancel="handleCancel">
<a-form :model="form" :rules="rules" :disabled="mode === 'show'" ref="dialogForm" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-form-item label="角色名称" name="name">
<a-input
v-model:value="form.name"
placeholder="请输入角色名称"
allow-clear
></a-input>
<a-input v-model:value="form.name" placeholder="请输入角色名称" allow-clear></a-input>
</a-form-item>
<a-form-item label="角色编码" name="code">
<a-input
v-model:value="form.code"
placeholder="请输入角色编码"
allow-clear
:disabled="mode === 'edit'"
></a-input>
<a-input v-model:value="form.code" placeholder="请输入角色编码" allow-clear :disabled="mode === 'edit'"></a-input>
</a-form-item>
<a-form-item label="角色描述" name="description">
<a-textarea
v-model:value="form.description"
placeholder="请输入角色描述"
:rows="4"
allow-clear
></a-textarea>
<a-textarea v-model:value="form.description" placeholder="请输入角色描述" :rows="4" allow-clear></a-textarea>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number
v-model:value="form.sort"
:min="0"
:step="1"
style="width: 100%"
placeholder="请输入排序"
/>
<a-input-number v-model:value="form.sort" :min="0" :step="1" style="width: 100%" placeholder="请输入排序" />
</a-form-item>
<a-form-item label="状态" name="status">
<sc-select
v-model:value="form.status"
source-type="dictionary"
dictionary-code="role_status"
placeholder="请选择状态"
allow-clear
/>
<sc-select v-model:value="form.status" source-type="dictionary" dictionary-code="role_status" placeholder="请选择状态" allow-clear />
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="handleCancel"> </a-button>
<a-button
v-if="mode !== 'show'"
type="primary"
:loading="isSaveing"
@click="submit"
> </a-button
>
<a-button v-if="mode !== 'show'" type="primary" :loading="isSaveing" @click="submit"> </a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { message } from "ant-design-vue";
import scSelect from "@/components/scSelect/index.vue";
import authApi from "@/api/auth";
import { ref, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import scSelect from '@/components/scSelect/index.vue'
import authApi from '@/api/auth'
const emit = defineEmits(["success", "closed"]);
const emit = defineEmits(['success', 'closed'])
const mode = ref("add");
const mode = ref('add')
const titleMap = {
add: "新增角色",
edit: "编辑角色",
show: "查看角色",
};
const visible = ref(false);
const isSaveing = ref(false);
add: '新增角色',
edit: '编辑角色',
show: '查看角色',
}
const visible = ref(false)
const isSaveing = ref(false)
// 表单数据
const form = reactive({
id: "",
name: "",
code: "",
description: "",
id: '',
name: '',
code: '',
description: '',
sort: 1,
status: null,
});
})
// 表单引用
const dialogForm = ref();
const dialogForm = ref()
// 验证规则
const rules = {
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
code: [
{ required: true, message: "请输入角色编码", trigger: "blur" },
{ required: true, message: '请输入角色编码', trigger: 'blur' },
{
pattern: /^[a-zA-Z0-9_]+$/,
message: "角色编码只能包含字母、数字和下划线",
trigger: "blur",
message: '角色编码只能包含字母、数字和下划线',
trigger: 'blur',
},
],
sort: [
{ required: true, message: "请输入排序", trigger: "change" },
{ type: "number", message: "排序必须为数字", trigger: "change" },
{ required: true, message: '请输入排序', trigger: 'change' },
{ type: 'number', message: '排序必须为数字', trigger: 'change' },
],
};
}
// 显示对话框
const open = (openMode = "add") => {
mode.value = openMode;
visible.value = true;
const open = (openMode = 'add') => {
mode.value = openMode
visible.value = true
return {
setData,
open,
close,
};
};
}
}
// 关闭对话框
const close = () => {
visible.value = false;
};
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit("closed");
visible.value = false;
};
emit('closed')
visible.value = false
}
// 表单提交方法
const submit = async () => {
try {
await dialogForm.value.validate();
isSaveing.value = true;
let res = {};
if (mode.value === "add") {
res = await authApi.roles.add.post(form);
await dialogForm.value.validate()
isSaveing.value = true
let res = {}
if (mode.value === 'add') {
res = await authApi.role.add.post(form)
} else {
res = await authApi.roles.edit.put(form.id, form);
res = await authApi.role.edit.put(form.id, form)
}
isSaveing.value = false;
isSaveing.value = false
if (res.code === 200) {
emit("success", form, mode.value);
visible.value = false;
message.success("操作成功");
emit('success', form, mode.value)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("表单验证失败", error);
isSaveing.value = false;
console.error('表单验证失败', error)
isSaveing.value = false
}
};
}
// 表单注入数据
const setData = (data) => {
form.id = data.id;
form.name = data.name;
form.code = data.code;
form.description = data.description || "";
form.sort = data.sort;
form.status = data.status !== undefined ? data.status : null;
};
form.id = data.id
form.name = data.name
form.code = data.code
form.description = data.description || ''
form.sort = data.sort
form.status = data.status !== undefined ? data.status : null
}
// 暴露方法给父组件
defineExpose({
open,
setData,
close,
});
})
</script>
<style></style>
+183 -288
View File
@@ -3,12 +3,7 @@
<div class="tool-bar">
<div class="left-panel">
<a-space>
<a-input
v-model:value="searchForm.keyword"
placeholder="角色名称"
allow-clear
style="width: 180px"
/>
<a-input v-model:value="searchForm.keyword" placeholder="角色名称" allow-clear style="width: 180px" />
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
搜索
@@ -27,16 +22,10 @@
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleBatchStatus">
<CheckCircleOutlined />批量启用/禁用
</a-menu-item>
<a-menu-item @click="handleBatchCopy">
<CopyOutlined />批量复制
</a-menu-item>
<a-menu-item @click="handleBatchStatus"> <CheckCircleOutlined />批量启用/禁用 </a-menu-item>
<a-menu-item @click="handleBatchCopy"> <CopyOutlined />批量复制 </a-menu-item>
<a-menu-divider />
<a-menu-item @click="handleBatchDelete" danger>
<DeleteOutlined />批量删除
</a-menu-item>
<a-menu-item @click="handleBatchDelete" danger> <DeleteOutlined />批量删除 </a-menu-item>
</a-menu>
</template>
</a-dropdown>
@@ -47,15 +36,9 @@
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleImport">
<ImportOutlined />角色
</a-menu-item>
<a-menu-item @click="handleExport">
<ExportOutlined />导出角色
</a-menu-item>
<a-menu-item @click="handleDownloadTemplate">
<DownloadOutlined />下载模板
</a-menu-item>
<a-menu-item @click="handleImport"> <ImportOutlined />导入角色 </a-menu-item>
<a-menu-item @click="handleExport"> <ExportOutlined />角色 </a-menu-item>
<a-menu-item @click="handleDownloadTemplate"> <DownloadOutlined />下载模板 </a-menu-item>
</a-menu>
</template>
</a-dropdown>
@@ -66,38 +49,16 @@
</div>
</div>
<div class="table-content">
<scTable
ref="tableRef"
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
:row-key="rowKey"
:row-selection="rowSelection"
@refresh="refreshTable"
@paginationChange="handlePaginationChange"
@select="handleSelectChange"
@selectAll="handleSelectAll"
>
<scTable ref="tableRef" :columns="columns" :data-source="tableData" :loading="loading" :pagination="pagination" :row-key="rowKey" :row-selection="rowSelection" @refresh="refreshTable" @paginationChange="handlePaginationChange" @select="handleSelectChange" @selectAll="handleSelectAll">
<template #status="{ record }">
<a-tag :color="record.status === 1 ? 'success' : 'error'">
{{ record.status === 1 ? "正常" : "禁用" }}
{{ record.status === 1 ? '正常' : '禁用' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button
type="link"
size="small"
@click="handleEdit(record)"
>编辑</a-button
>
<a-button
type="link"
size="small"
@click="handleCopy(record)"
>复制</a-button
>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" @click="handleCopy(record)">复制</a-button>
<a-dropdown>
<a-button type="link" size="small">
更多
@@ -105,21 +66,10 @@
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleView(record)">
<SearchOutlined />查看
</a-menu-item>
<a-menu-item
@click="handlePermission(record)"
>
<ImportOutlined />权限
</a-menu-item>
<a-menu-item @click="handleView(record)"> <SearchOutlined />查看 </a-menu-item>
<a-menu-item @click="handlePermission(record)"> <ImportOutlined />权限 </a-menu-item>
<a-menu-divider />
<a-menu-item
@click="handleDelete(record)"
danger
>
<DeleteOutlined />删除
</a-menu-item>
<a-menu-item @click="handleDelete(record)" danger> <DeleteOutlined />删除 </a-menu-item>
</a-menu>
</template>
</a-dropdown>
@@ -130,104 +80,49 @@
</div>
<!-- 新增/编辑角色弹窗 -->
<save-dialog
v-if="dialog.save"
ref="saveDialogRef"
@success="handleSaveSuccess"
@closed="dialog.save = false"
/>
<save-dialog v-if="dialog.save" ref="saveDialogRef" @success="handleSaveSuccess" @closed="dialog.save = false" />
<!-- 权限设置弹窗 -->
<permission-dialog
v-if="dialog.permission"
ref="permissionDialogRef"
@success="permissionSuccess"
@closed="dialog.permission = false"
/>
<permission-dialog v-if="dialog.permission" ref="permissionDialogRef" @success="permissionSuccess" @closed="dialog.permission = false" />
<!-- 导入角色弹窗 -->
<sc-import
v-model:open="dialog.import"
title="导入角色"
:api="authApi.roles.import.post"
:template-api="authApi.roles.downloadTemplate.get"
filename="角色"
@success="handleImportSuccess"
/>
<sc-import v-model:open="dialog.import" title="导入角色" :api="authApi.role.import.post" :template-api="authApi.role.downloadTemplate.get" filename="角色" @success="handleImportSuccess" />
<!-- 导出角色弹窗 -->
<sc-export
v-model:open="dialog.export"
title="导出角色"
:api="handleExportApi"
:default-filename="`角色列表_${Date.now()}`"
:show-options="false"
tip="导出当前选中或所有角色数据"
@success="handleExportSuccess"
/>
<sc-export v-model:open="dialog.export" title="导出角色" :api="handleExportApi" :default-filename="`角色列表_${Date.now()}`" :show-options="false" tip="导出当前选中或所有角色数据" @success="handleExportSuccess" />
<!-- 复制角色弹窗 -->
<copy-dialog
v-if="dialog.copy"
ref="copyDialogRef"
@success="handleCopySuccess"
@closed="dialog.copy = false"
/>
<copy-dialog v-if="dialog.copy" ref="copyDialogRef" @success="handleCopySuccess" @closed="dialog.copy = false" />
</template>
<script setup>
import { ref, reactive } from "vue";
import { message, Modal } from "ant-design-vue";
import {
SearchOutlined,
RedoOutlined,
PlusOutlined,
DownOutlined,
CheckCircleOutlined,
CopyOutlined,
DeleteOutlined,
ImportOutlined,
ExportOutlined,
DownloadOutlined,
} from "@ant-design/icons-vue";
import scTable from "@/components/scTable/index.vue";
import scImport from "@/components/scImport/index.vue";
import scExport from "@/components/scExport/index.vue";
import saveDialog from "./components/SaveDialog.vue";
import permissionDialog from "./components/PermissionDialog.vue";
import copyDialog from "./components/CopyDialog.vue";
import authApi from "@/api/auth";
import { useTable } from "@/hooks/useTable";
import { ref, reactive } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { SearchOutlined, RedoOutlined, PlusOutlined, DownOutlined, CheckCircleOutlined, CopyOutlined, DeleteOutlined, ImportOutlined, ExportOutlined, DownloadOutlined } from '@ant-design/icons-vue'
import scTable from '@/components/scTable/index.vue'
import scImport from '@/components/scImport/index.vue'
import scExport from '@/components/scExport/index.vue'
import saveDialog from './components/SaveDialog.vue'
import permissionDialog from './components/PermissionDialog.vue'
import copyDialog from './components/CopyDialog.vue'
import authApi from '@/api/auth'
import { useTable } from '@/hooks/useTable'
defineOptions({
name: "authRole",
});
name: 'authRole',
})
// 使用useTable hooks
const {
tableRef,
searchForm,
tableData,
loading,
pagination,
selectedRows,
rowSelection,
handleSearch,
handleReset,
handlePaginationChange,
handleSelectChange,
handleSelectAll,
refreshTable,
} = useTable({
api: authApi.roles.list.get,
const { tableRef, searchForm, tableData, loading, pagination, selectedRows, rowSelection, handleSearch, handleReset, handlePaginationChange, handleSelectChange, handleSelectAll, refreshTable } = useTable({
api: authApi.role.list.get,
searchForm: {
keyword: "",
keyword: '',
status: null,
},
columns: [],
needPagination: true,
needSelection: true,
});
})
// 对话框状态
const dialog = reactive({
@@ -236,287 +131,287 @@ const dialog = reactive({
import: false,
export: false,
copy: false,
});
})
// 弹窗引用
const saveDialogRef = ref(null);
const permissionDialogRef = ref(null);
const copyDialogRef = ref(null);
const saveDialogRef = ref(null)
const permissionDialogRef = ref(null)
const copyDialogRef = ref(null)
// 行key
const rowKey = "id";
const rowKey = 'id'
// 表格列配置
const columns = [
{ title: "ID", dataIndex: "id", key: "id", width: 80, align: "center" },
{ title: "角色名称", dataIndex: "name", key: "name", width: 200 },
{ title: "角色编码", dataIndex: "code", key: "code", width: 200 },
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80, align: 'center' },
{ title: '角色名称', dataIndex: 'name', key: 'name', width: 200 },
{ title: '角色编码', dataIndex: 'code', key: 'code', width: 200 },
{
title: "描述",
dataIndex: "description",
key: "description",
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: "排序",
dataIndex: "sort",
key: "sort",
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 100,
align: "center",
align: 'center',
},
{
title: "状态",
dataIndex: "status",
key: "status",
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: "center",
slot: "status",
align: 'center',
slot: 'status',
},
{
title: "操作",
dataIndex: "action",
key: "action",
title: '操作',
dataIndex: 'action',
key: 'action',
width: 180,
align: "center",
slot: "action",
fixed: "right",
align: 'center',
slot: 'action',
fixed: 'right',
},
];
]
// 新增角色
const handleAdd = () => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("add");
}, 0);
};
saveDialogRef.value?.open('add')
}, 0)
}
// 查看角色
const handleView = (record) => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("show").setData(record);
}, 0);
};
saveDialogRef.value?.open('show').setData(record)
}, 0)
}
// 编辑角色
const handleEdit = (record) => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("edit").setData(record);
}, 0);
};
saveDialogRef.value?.open('edit').setData(record)
}, 0)
}
// 删除角色
const handleDelete = (record) => {
Modal.confirm({
title: "确认删除",
content: "确定删除该角色吗?",
okText: "确定",
cancelText: "取消",
okType: "danger",
title: '确认删除',
content: '确定删除该角色吗?',
okText: '确定',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
try {
const res = await authApi.roles.delete.delete(record.id);
const res = await authApi.role.delete.delete(record.id)
if (res.code === 200) {
message.success("删除成功");
refreshTable();
message.success('删除成功')
refreshTable()
} else {
message.error(res.message || "删除失败");
message.error(res.message || '删除失败')
}
} catch (error) {
console.error("删除角色失败:", error);
message.error("删除失败");
console.error('删除角色失败:', error)
message.error('删除失败')
}
},
});
};
})
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要删除的角色");
return;
message.warning('请选择要删除的角色')
return
}
Modal.confirm({
title: "确认删除",
title: '确认删除',
content: `确定删除选中的 ${selectedRows.value.length} 个角色吗?`,
okText: "确定",
cancelText: "取消",
okType: "danger",
okText: '确定',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
try {
const ids = selectedRows.value.map((item) => item.id);
const res = await authApi.roles.batchDelete.post({ ids });
const ids = selectedRows.value.map((item) => item.id)
const res = await authApi.role.batchDelete.post({ ids })
if (res.code === 200) {
message.success("删除成功");
selectedRows.value = [];
refreshTable();
message.success('删除成功')
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || "删除失败");
message.error(res.message || '删除失败')
}
} catch (error) {
console.error("批量删除角色失败:", error);
message.error("删除失败");
console.error('批量删除角色失败:', error)
message.error('删除失败')
}
},
});
};
})
}
// 批量更新状态
const handleBatchStatus = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要操作的角色");
return;
message.warning('请选择要操作的角色')
return
}
Modal.confirm({
title: "确认操作",
content: "确定要批量启用/禁用选中的角色吗?",
okText: "确定",
cancelText: "取消",
title: '确认操作',
content: '确定要批量启用/禁用选中的角色吗?',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
const ids = selectedRows.value.map((item) => item.id);
const status = selectedRows.value[0].status === 1 ? 0 : 1;
const res = await authApi.roles.batchStatus.post({
const ids = selectedRows.value.map((item) => item.id)
const status = selectedRows.value[0].status === 1 ? 0 : 1
const res = await authApi.role.batchStatus.post({
ids,
status,
});
})
if (res.code === 200) {
message.success("操作成功");
selectedRows.value = [];
refreshTable();
message.success('操作成功')
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("批量更新状态失败:", error);
message.error("操作失败");
console.error('批量更新状态失败:', error)
message.error('操作失败')
}
},
});
};
})
}
// 复制角色
const handleCopy = (record) => {
dialog.copy = true;
dialog.copy = true
setTimeout(() => {
copyDialogRef.value?.open(record);
}, 0);
};
copyDialogRef.value?.open(record)
}, 0)
}
// 批量复制角色
const handleBatchCopy = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要复制的角色");
return;
message.warning('请选择要复制的角色')
return
}
Modal.confirm({
title: "确认批量复制",
title: '确认批量复制',
content: `确定复制选中的 ${selectedRows.value.length} 个角色吗?`,
okText: "确定",
cancelText: "取消",
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
const ids = selectedRows.value.map((item) => item.id);
const res = await authApi.roles.batchCopy.post({ ids });
const ids = selectedRows.value.map((item) => item.id)
const res = await authApi.role.batchCopy.post({ ids })
if (res.code === 200) {
message.success("批量复制成功");
selectedRows.value = [];
refreshTable();
message.success('批量复制成功')
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || "批量复制失败");
message.error(res.message || '批量复制失败')
}
} catch (error) {
console.error("批量复制角色失败:", error);
message.error("批量复制失败");
console.error('批量复制角色失败:', error)
message.error('批量复制失败')
}
},
});
};
})
}
// 复制成功回调
const handleCopySuccess = () => {
refreshTable();
};
refreshTable()
}
// 权限设置
const handlePermission = (record) => {
if (!record && selectedRows.value.length !== 1) {
message.error("请选择一个角色进行权限设置");
return;
message.error('请选择一个角色进行权限设置')
return
}
const roleData = record || selectedRows.value[0];
dialog.permission = true;
const roleData = record || selectedRows.value[0]
dialog.permission = true
setTimeout(() => {
permissionDialogRef.value?.open().setData(roleData);
}, 0);
};
permissionDialogRef.value?.open().setData(roleData)
}, 0)
}
// 重置
const handleUserReset = () => {
searchForm.keyword = "";
searchForm.status = null;
handleSearch();
};
searchForm.keyword = ''
searchForm.status = null
handleSearch()
}
// 保存成功回调
const handleSaveSuccess = () => {
refreshTable();
};
refreshTable()
}
// 权限设置成功回调
const permissionSuccess = () => {
refreshTable();
};
refreshTable()
}
// 导出角色
const handleExport = async () => {
dialog.export = true;
};
dialog.export = true
}
// 导出API封装
const handleExportApi = async () => {
const ids = selectedRows.value.map((item) => item.id);
return await authApi.roles.export.post({
const ids = selectedRows.value.map((item) => item.id)
return await authApi.role.export.post({
ids: ids.length > 0 ? ids : undefined,
});
};
})
}
// 导出成功回调
const handleExportSuccess = () => {
selectedRows.value = [];
};
selectedRows.value = []
}
// 导入角色
const handleImport = () => {
dialog.import = true;
};
dialog.import = true
}
// 导入成功回调
const handleImportSuccess = () => {
refreshTable();
};
refreshTable()
}
// 下载模板
const handleDownloadTemplate = async () => {
try {
const blob = await authApi.roles.downloadTemplate.get();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "角色导入模板.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success("下载成功");
const blob = await authApi.role.downloadTemplate.get()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '角色导入模板.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('下载成功')
} catch (error) {
console.error("下载模板失败:", error);
message.error("下载失败");
console.error('下载模板失败:', error)
message.error('下载失败')
}
};
}
</script>
@@ -1,110 +1,94 @@
<template>
<a-modal
v-model:open="visible"
title="批量分配角色"
:confirm-loading="loading"
@ok="handleOk"
@cancel="handleCancel"
>
<a-modal v-model:open="visible" title="批量分配角色" :confirm-loading="loading" @ok="handleOk" @cancel="handleCancel">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="角色">
<a-select
v-model:value="selectedRoleIds"
mode="multiple"
placeholder="请选择角色"
allow-clear
:options="roleOptions"
:field-names="{ label: 'name', value: 'id' }"
:loading="roleLoading"
show-search
:filter-option="filterOption"
/>
<a-select v-model:value="selectedRoleIds" mode="multiple" placeholder="请选择角色" allow-clear :options="roleOptions" :field-names="{ label: 'name', value: 'id' }" :loading="roleLoading" show-search :filter-option="filterOption" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref } from "vue";
import { message } from "ant-design-vue";
import authApi from "@/api/auth";
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const visible = ref(false);
const loading = ref(false);
const roleLoading = ref(false);
const roleOptions = ref([]);
const userIds = ref([]);
const selectedRoleIds = ref([]);
const visible = ref(false)
const loading = ref(false)
const roleLoading = ref(false)
const roleOptions = ref([])
const userIds = ref([])
const selectedRoleIds = ref([])
// 打开弹窗
const open = async (ids) => {
visible.value = true;
userIds.value = ids;
selectedRoleIds.value = [];
await loadRoles();
};
visible.value = true
userIds.value = ids
selectedRoleIds.value = []
await loadRoles()
}
// 加载角色列表
const loadRoles = async () => {
try {
roleLoading.value = true;
const res = await authApi.roles.list.get({ page_size: 1000 });
roleLoading.value = true
const res = await authApi.role.list.get({ page_size: 1000 })
if (res.code === 200) {
roleOptions.value = (res.data.list || []).map((role) => ({
id: role.id,
name: role.name,
code: role.code,
}));
}))
}
} catch (error) {
console.error("加载角色列表失败:", error);
console.error('加载角色列表失败:', error)
} finally {
roleLoading.value = false;
roleLoading.value = false
}
};
}
// 角色过滤
const filterOption = (input, option) => {
const name = option?.name?.toLowerCase() || "";
const code = option?.code?.toLowerCase() || "";
const keyword = input?.toLowerCase() || "";
return name.includes(keyword) || code.includes(keyword);
};
const name = option?.name?.toLowerCase() || ''
const code = option?.code?.toLowerCase() || ''
const keyword = input?.toLowerCase() || ''
return name.includes(keyword) || code.includes(keyword)
}
// 确认
const handleOk = async () => {
try {
loading.value = true;
const res = await authApi.users.batchRoles.post({
loading.value = true
const res = await authApi.user.batchRoles.post({
ids: userIds.value,
role_ids: selectedRoleIds.value,
});
})
if (res.code === 200) {
message.success("分配成功");
emit("success");
handleCancel();
message.success('分配成功')
emit('success')
handleCancel()
} else {
message.error(res.message || "分配失败");
message.error(res.message || '分配失败')
}
} catch (error) {
console.error("批量分配角色失败:", error);
message.error(error.message || "分配失败");
console.error('批量分配角色失败:', error)
message.error(error.message || '分配失败')
} finally {
loading.value = false;
loading.value = false
}
};
}
// 取消
const handleCancel = () => {
visible.value = false;
loading.value = false;
selectedRoleIds.value = [];
};
visible.value = false
loading.value = false
selectedRoleIds.value = []
}
const emit = defineEmits(["success"]);
const emit = defineEmits(['success'])
defineExpose({
open,
});
})
</script>
@@ -1,16 +1,6 @@
<template>
<a-modal
v-model:open="visible"
title="批量分配部门"
:confirm-loading="loading"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form
:model="formState"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
>
<a-modal v-model:open="visible" title="批量分配部门" :confirm-loading="loading" @ok="handleOk" @cancel="handleCancel">
<a-form :model="formState" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="部门">
<a-tree-select
v-model:value="formState.department_id"
@@ -32,84 +22,84 @@
</template>
<script setup>
import { ref, reactive } from "vue";
import { message } from "ant-design-vue";
import authApi from "@/api/auth";
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const visible = ref(false);
const loading = ref(false);
const departmentTree = ref([]);
const userIds = ref([]);
const visible = ref(false)
const loading = ref(false)
const departmentTree = ref([])
const userIds = ref([])
const formState = reactive({
department_id: undefined,
});
})
// 打开弹窗
const open = (ids) => {
visible.value = true;
userIds.value = ids;
formState.department_id = undefined;
loadDepartmentTree();
};
visible.value = true
userIds.value = ids
formState.department_id = undefined
loadDepartmentTree()
}
// 加载部门树
const loadDepartmentTree = async () => {
try {
const res = await authApi.departments.tree.get();
const res = await authApi.department.tree.get()
if (res.code === 200) {
departmentTree.value = res.data || [];
departmentTree.value = res.data || []
}
} catch (error) {
console.error("加载部门树失败:", error);
console.error('加载部门树失败:', error)
}
};
}
// 树节点过滤
const filterTreeNode = (inputValue, treeNode) => {
const name = treeNode.dataRef.name;
return name ? name.toLowerCase().includes(inputValue.toLowerCase()) : false;
};
const name = treeNode.dataRef.name
return name ? name.toLowerCase().includes(inputValue.toLowerCase()) : false
}
// 确认
const handleOk = async () => {
if (!formState.department_id) {
message.warning("请选择部门");
return;
message.warning('请选择部门')
return
}
try {
loading.value = true;
const res = await authApi.users.batchDepartment.post({
loading.value = true
const res = await authApi.user.batchDepartment.post({
ids: userIds.value,
department_id: formState.department_id,
});
})
if (res.code === 200) {
message.success("分配成功");
emit("success");
handleCancel();
message.success('分配成功')
emit('success')
handleCancel()
} else {
message.error(res.message || "分配失败");
message.error(res.message || '分配失败')
}
} catch (error) {
console.error("批量分配部门失败:", error);
message.error(error.message || "分配失败");
console.error('批量分配部门失败:', error)
message.error(error.message || '分配失败')
} finally {
loading.value = false;
loading.value = false
}
};
}
// 取消
const handleCancel = () => {
visible.value = false;
loading.value = false;
formState.department_id = undefined;
};
visible.value = false
loading.value = false
formState.department_id = undefined
}
const emit = defineEmits(["success"]);
const emit = defineEmits(['success'])
defineExpose({
open,
});
})
</script>
@@ -1,131 +1,115 @@
<template>
<a-modal
v-model:open="visible"
title="设置角色"
:confirm-loading="loading"
@ok="handleOk"
@cancel="handleCancel"
>
<a-modal v-model:open="visible" title="设置角色" :confirm-loading="loading" @ok="handleOk" @cancel="handleCancel">
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item label="用户">
<a-input :value="userForm.username" disabled />
</a-form-item>
<a-form-item label="角色">
<a-select
v-model:value="selectedRoleIds"
mode="multiple"
placeholder="请选择角色"
allow-clear
:options="roleOptions"
:field-names="{ label: 'name', value: 'id' }"
:loading="roleLoading"
show-search
:filter-option="filterOption"
/>
<a-select v-model:value="selectedRoleIds" mode="multiple" placeholder="请选择角色" allow-clear :options="roleOptions" :field-names="{ label: 'name', value: 'id' }" :loading="roleLoading" show-search :filter-option="filterOption" />
</a-form-item>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive } from "vue";
import { message } from "ant-design-vue";
import authApi from "@/api/auth";
import { ref, reactive } from 'vue'
import { message } from 'ant-design-vue'
import authApi from '@/api/auth'
const visible = ref(false);
const loading = ref(false);
const roleLoading = ref(false);
const roleOptions = ref([]);
const selectedRoleIds = ref([]);
const userId = ref(null);
const visible = ref(false)
const loading = ref(false)
const roleLoading = ref(false)
const roleOptions = ref([])
const selectedRoleIds = ref([])
const userId = ref(null)
const userForm = reactive({
username: "",
});
username: '',
})
// 打开弹窗
const open = () => {
visible.value = true;
loadRoles();
};
visible.value = true
loadRoles()
}
// 设置用户数据
const setData = (user) => {
userId.value = user.id;
userForm.username = user.username;
userId.value = user.id
userForm.username = user.username
// 设置已选择的角色
selectedRoleIds.value = (user.roles || []).map((role) => role.id);
};
selectedRoleIds.value = (user.roles || []).map((role) => role.id)
}
// 加载角色列表
const loadRoles = async () => {
try {
roleLoading.value = true;
const res = await authApi.roles.list.get({ page_size: 1000 });
roleLoading.value = true
const res = await authApi.role.list.get({ page_size: 1000 })
if (res.code === 200) {
roleOptions.value = (res.data.list || []).map((role) => ({
id: role.id,
name: role.name,
code: role.code,
}));
}))
}
} catch (error) {
console.error("加载角色列表失败:", error);
console.error('加载角色列表失败:', error)
} finally {
roleLoading.value = false;
roleLoading.value = false
}
};
}
// 角色过滤
const filterOption = (input, option) => {
const name = option?.name?.toLowerCase() || "";
const code = option?.code?.toLowerCase() || "";
const keyword = input?.toLowerCase() || "";
return name.includes(keyword) || code.includes(keyword);
};
const name = option?.name?.toLowerCase() || ''
const code = option?.code?.toLowerCase() || ''
const keyword = input?.toLowerCase() || ''
return name.includes(keyword) || code.includes(keyword)
}
// 确认
const handleOk = async () => {
if (!userId.value) {
message.warning("用户ID不能为空");
return;
message.warning('用户ID不能为空')
return
}
try {
loading.value = true;
const res = await authApi.users.batchRoles.post({
loading.value = true
const res = await authApi.user.batchRoles.post({
ids: [userId.value],
role_ids: selectedRoleIds.value,
});
})
if (res.code === 200) {
message.success("设置成功");
emit("success");
handleCancel();
message.success('设置成功')
emit('success')
handleCancel()
} else {
message.error(res.message || "设置失败");
message.error(res.message || '设置失败')
}
} catch (error) {
console.error("设置角色失败:", error);
message.error(error.message || "设置失败");
console.error('设置角色失败:', error)
message.error(error.message || '设置失败')
} finally {
loading.value = false;
loading.value = false
}
};
}
// 取消
const handleCancel = () => {
visible.value = false;
loading.value = false;
userId.value = null;
userForm.username = "";
selectedRoleIds.value = [];
};
visible.value = false
loading.value = false
userId.value = null
userForm.username = ''
selectedRoleIds.value = []
}
const emit = defineEmits(["success"]);
const emit = defineEmits(['success'])
defineExpose({
open,
setData,
});
})
</script>
@@ -1,293 +1,210 @@
<template>
<a-modal
:title="titleMap[mode]"
:open="visible"
:width="500"
:destroy-on-close="true"
:mask-closable="false"
@cancel="handleCancel"
>
<a-form
:model="form"
:rules="rules"
:disabled="mode === 'show'"
ref="dialogForm"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 18 }"
>
<a-modal :title="titleMap[mode]" :open="visible" :width="500" :destroy-on-close="true" :mask-closable="false" @cancel="handleCancel">
<a-form :model="form" :rules="rules" :disabled="mode === 'show'" ref="dialogForm" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-form-item label="头像" name="avatar">
<sc-upload
v-model="form.avatar"
:cropper="true"
:aspectRatio="1"
title="上传头像"
></sc-upload>
<sc-upload v-model="form.avatar" :cropper="true" :aspectRatio="1" title="上传头像"></sc-upload>
</a-form-item>
<a-form-item label="用户名" name="username">
<a-input
v-model:value="form.username"
placeholder="请输入用户名"
allow-clear
:disabled="mode === 'edit'"
/>
<a-input v-model:value="form.username" placeholder="请输入用户名" allow-clear :disabled="mode === 'edit'" />
</a-form-item>
<a-form-item label="真实姓名" name="real_name">
<a-input
v-model:value="form.real_name"
placeholder="请输入真实姓名"
allow-clear
/>
<a-input v-model:value="form.real_name" placeholder="请输入真实姓名" allow-clear />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input
v-model:value="form.email"
placeholder="请输入邮箱"
allow-clear
/>
<a-input v-model:value="form.email" placeholder="请输入邮箱" allow-clear />
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input
v-model:value="form.phone"
placeholder="请输入手机号"
allow-clear
/>
<a-input v-model:value="form.phone" placeholder="请输入手机号" allow-clear />
</a-form-item>
<template v-if="mode === 'add'">
<a-form-item label="登录密码" name="password">
<a-input-password
v-model:value="form.password"
placeholder="请输入登录密码"
allow-clear
/>
<a-input-password v-model:value="form.password" placeholder="请输入登录密码" allow-clear />
</a-form-item>
<a-form-item label="确认密码" name="password2">
<a-input-password
v-model:value="form.password2"
placeholder="请再次输入密码"
allow-clear
/>
<a-input-password v-model:value="form.password2" placeholder="请再次输入密码" allow-clear />
</a-form-item>
</template>
<a-form-item label="所属部门" name="department_id">
<a-tree-select
v-model:value="form.department_id"
:tree-data="department"
:field-names="departmentFieldNames"
:tree-default-expand-all="false"
show-icon
placeholder="请选择部门"
allow-clear
tree-node-filter-prop="name"
/>
<a-tree-select v-model:value="form.department_id" :tree-data="department" :field-names="departmentFieldNames" :tree-default-expand-all="false" show-icon placeholder="请选择部门" allow-clear tree-node-filter-prop="name" />
</a-form-item>
<a-form-item label="所属角色" name="role_ids">
<a-select
v-model:value="form.role_ids"
mode="multiple"
placeholder="请选择角色"
allow-clear
style="width: 100%"
>
<a-select-option
v-for="role in rolesList"
:key="role.id"
:value="role.id"
>
<a-select v-model:value="form.role_ids" mode="multiple" placeholder="请选择角色" allow-clear style="width: 100%">
<a-select-option v-for="role in rolesList" :key="role.id" :value="role.id">
{{ role.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="性别" name="gender">
<sc-select
v-model:value="form.gender"
source-type="dictionary"
dictionary-code="gender"
placeholder="请选择性别"
allow-clear
/>
<sc-select v-model:value="form.gender" source-type="dictionary" dictionary-code="gender" placeholder="请选择性别" allow-clear />
</a-form-item>
<a-form-item label="状态" name="status">
<sc-select
v-model:value="form.status"
source-type="dictionary"
dictionary-code="user_status"
placeholder="请选择状态"
allow-clear
/>
<sc-select v-model:value="form.status" source-type="dictionary" dictionary-code="user_status" placeholder="请选择状态" allow-clear />
</a-form-item>
</a-form>
<template #footer>
<a-button @click="handleCancel"> </a-button>
<a-button
v-if="mode !== 'show'"
type="primary"
:loading="isSaveing"
@click="submit"
> </a-button
>
<a-button v-if="mode !== 'show'" type="primary" :loading="isSaveing" @click="submit"> </a-button>
</template>
</a-modal>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { message } from "ant-design-vue";
import scUpload from "@/components/scUpload/index.vue";
import scSelect from "@/components/scSelect/index.vue";
import authApi from "@/api/auth";
import { ref, reactive, computed } from 'vue'
import { message } from 'ant-design-vue'
import scUpload from '@/components/scUpload/index.vue'
import scSelect from '@/components/scSelect/index.vue'
import authApi from '@/api/auth'
const emit = defineEmits(["success", "closed"]);
const emit = defineEmits(['success', 'closed'])
const mode = ref("add");
const mode = ref('add')
const titleMap = {
add: "新增用户",
edit: "编辑用户",
show: "查看用户",
};
const visible = ref(false);
const isSaveing = ref(false);
add: '新增用户',
edit: '编辑用户',
show: '查看用户',
}
const visible = ref(false)
const isSaveing = ref(false)
// 表单数据
const form = reactive({
id: "",
username: "",
avatar: "",
real_name: "",
email: "",
phone: "",
id: '',
username: '',
avatar: '',
real_name: '',
email: '',
phone: '',
department_id: null,
role_ids: [],
gender: null,
status: null,
});
})
// 表单引用
const dialogForm = ref();
const dialogForm = ref()
// 验证规则
const rules = {
username: [
{ required: true, message: "请输入用户名", trigger: "blur" },
{ required: true, message: '请输入用户名', trigger: 'blur' },
{
min: 3,
max: 50,
message: "用户名长度在 3 到 50 个字符",
trigger: "blur",
message: '用户名长度在 3 到 50 个字符',
trigger: 'blur',
},
],
real_name: [
{ required: true, message: "请输入真实姓名", trigger: "blur" },
{ required: true, message: '请输入真实姓名', trigger: 'blur' },
{
min: 2,
max: 50,
message: "真实姓名长度在 2 到 50 个字符",
trigger: "blur",
message: '真实姓名长度在 2 到 50 个字符',
trigger: 'blur',
},
],
email: [
{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" },
],
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }],
phone: [
{
pattern: /^1[3-9]\d{9}$/,
message: "请输入正确的手机号",
trigger: "blur",
message: '请输入正确的手机号',
trigger: 'blur',
},
],
password: [
{ required: true, message: "请输入登录密码", trigger: "blur" },
{ required: true, message: '请输入登录密码', trigger: 'blur' },
{
min: 6,
max: 20,
message: "密码长度在 6 到 20 个字符",
trigger: "blur",
message: '密码长度在 6 到 20 个字符',
trigger: 'blur',
},
{
validator: (rule, value) => {
if (form.password2 !== "") {
dialogForm.value?.validateFields("password2");
if (form.password2 !== '') {
dialogForm.value?.validateFields('password2')
}
return Promise.resolve();
return Promise.resolve()
},
trigger: "change",
trigger: 'change',
},
],
password2: [
{ required: true, message: "请再次输入密码", trigger: "blur" },
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (rule, value) => {
if (value !== form.password) {
return Promise.reject(new Error("两次输入密码不一致!"));
return Promise.reject(new Error('两次输入密码不一致!'))
}
return Promise.resolve();
return Promise.resolve()
},
trigger: "blur",
trigger: 'blur',
},
],
};
}
// 部门数据
const department = ref([]);
const department = ref([])
const departmentFieldNames = {
value: "id",
label: "name",
children: "children",
};
value: 'id',
label: 'name',
children: 'children',
}
// 角色列表
const rolesList = ref([]);
const rolesList = ref([])
// 显示对话框
const open = (openMode = "add") => {
mode.value = openMode;
visible.value = true;
const open = (openMode = 'add') => {
mode.value = openMode
visible.value = true
return {
setData,
open,
close,
};
};
}
}
// 关闭对话框
const close = () => {
visible.value = false;
};
visible.value = false
}
// 处理取消
const handleCancel = () => {
emit("closed");
visible.value = false;
};
emit('closed')
visible.value = false
}
// 加载部门树数据
const loadDepartment = async () => {
try {
const res = await authApi.departments.tree.get();
const res = await authApi.department.tree.get()
if (res.code === 200) {
department.value = res.data || [];
department.value = res.data || []
}
} catch (error) {
console.error("加载部门树失败:", error);
console.error('加载部门树失败:', error)
}
};
}
// 加载角色列表
const loadRoles = async () => {
try {
const res = await authApi.roles.all.get();
const res = await authApi.role.all.get()
if (res.code === 200) {
rolesList.value = res.data || [];
rolesList.value = res.data || []
}
} catch (error) {
console.error("加载角色列表失败:", error);
console.error('加载角色列表失败:', error)
}
};
}
// 表单提交方法
const submit = async () => {
try {
await dialogForm.value.validate();
isSaveing.value = true;
await dialogForm.value.validate()
isSaveing.value = true
const submitData = {
username: form.username,
@@ -299,57 +216,57 @@ const submit = async () => {
role_ids: form.role_ids,
gender: form.gender,
status: form.status,
};
if (mode.value === "add") {
submitData.password = form.password;
}
let res = {};
if (mode.value === "add") {
res = await authApi.users.add.post(submitData);
if (mode.value === 'add') {
submitData.password = form.password
}
let res = {}
if (mode.value === 'add') {
res = await authApi.user.add.post(submitData)
} else {
res = await authApi.users.edit.put(form.id, submitData);
res = await authApi.user.edit.put(form.id, submitData)
}
isSaveing.value = false;
isSaveing.value = false
if (res.code === 200) {
emit("success", form, mode.value);
visible.value = false;
message.success("操作成功");
emit('success', form, mode.value)
visible.value = false
message.success('操作成功')
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("表单验证失败", error);
isSaveing.value = false;
console.error('表单验证失败', error)
isSaveing.value = false
}
};
}
// 表单注入数据
const setData = (data) => {
form.id = data.id;
form.username = data.username;
form.avatar = data.avatar;
form.real_name = data.real_name;
form.email = data.email;
form.phone = data.phone;
form.department_id = data.department_id;
form.role_ids = data.roles ? data.roles.map((item) => item.id) : [];
form.gender = data.gender !== undefined ? data.gender : null;
form.status = data.status !== undefined ? data.status : null;
};
form.id = data.id
form.username = data.username
form.avatar = data.avatar
form.real_name = data.real_name
form.email = data.email
form.phone = data.phone
form.department_id = data.department_id
form.role_ids = data.roles ? data.roles.map((item) => item.id) : []
form.gender = data.gender !== undefined ? data.gender : null
form.status = data.status !== undefined ? data.status : null
}
// 组件挂载时加载数据
loadDepartment();
loadRoles();
loadDepartment()
loadRoles()
// 暴露方法给父组件
defineExpose({
open,
setData,
close,
});
})
</script>
<style></style>
+252 -384
View File
@@ -2,12 +2,7 @@
<div class="pages-sidebar-layout user-page">
<div class="left-box">
<div class="header">
<a-input
v-model:value="departmentKeyword"
placeholder="搜索部门..."
allow-clear
@change="handleDeptSearch"
>
<a-input v-model:value="departmentKeyword" placeholder="搜索部门..." allow-clear @change="handleDeptSearch">
<template #prefix>
<SearchOutlined style="color: rgba(0, 0, 0, 0.45)" />
</template>
@@ -28,11 +23,7 @@
@select="onDeptSelect"
>
<template #icon="{ dataRef }">
<ApartmentOutlined
v-if="
dataRef.children && dataRef.children.length > 0
"
/>
<ApartmentOutlined v-if="dataRef.children && dataRef.children.length > 0" />
<UserOutlined v-else />
</template>
</a-tree>
@@ -43,12 +34,7 @@
<div class="tool-bar">
<div class="left-panel">
<a-space>
<a-input
v-model:value="searchForm.username"
placeholder="用户名"
allow-clear
style="width: 140px"
/>
<a-input v-model:value="searchForm.username" placeholder="用户名" allow-clear style="width: 140px" />
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
搜索
@@ -67,19 +53,11 @@
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleBatchStatus">
<CheckCircleOutlined />批量启用/禁用
</a-menu-item>
<a-menu-item @click="handleBatchDepartment">
<ApartmentOutlined />批量分配部门
</a-menu-item>
<a-menu-item @click="handleBatchRoles">
<TeamOutlined />批量分配角色
</a-menu-item>
<a-menu-item @click="handleBatchStatus"> <CheckCircleOutlined />批量启用/禁用 </a-menu-item>
<a-menu-item @click="handleBatchDepartment"> <ApartmentOutlined />批量分配部门 </a-menu-item>
<a-menu-item @click="handleBatchRoles"> <TeamOutlined />批量分配角色 </a-menu-item>
<a-menu-divider />
<a-menu-item @click="handleBatchDelete" danger>
<DeleteOutlined />批量删除
</a-menu-item>
<a-menu-item @click="handleBatchDelete" danger> <DeleteOutlined />批量删除 </a-menu-item>
</a-menu>
</template>
</a-dropdown>
@@ -90,15 +68,9 @@
</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleImport">
<ImportOutlined />导入用户
</a-menu-item>
<a-menu-item @click="handleExport">
<ExportOutlined />导出用户
</a-menu-item>
<a-menu-item @click="handleDownloadTemplate">
<DownloadOutlined />下载模板
</a-menu-item>
<a-menu-item @click="handleImport"> <ImportOutlined />导入用户 </a-menu-item>
<a-menu-item @click="handleExport"> <ExportOutlined />导出用户 </a-menu-item>
<a-menu-item @click="handleDownloadTemplate"> <DownloadOutlined />下载模板 </a-menu-item>
</a-menu>
</template>
</a-dropdown>
@@ -128,57 +100,26 @@
</a-avatar>
</template>
<template #status="{ record }">
<a-tag
:color="record.status === 1 ? 'success' : 'error'"
>
{{ record.status === 1 ? "正常" : "禁用" }}
<a-tag :color="record.status === 1 ? 'success' : 'error'">
{{ record.status === 1 ? '正常' : '禁用' }}
</a-tag>
</template>
<template #department="{ record }">
{{ record.department?.name || "-" }}
{{ record.department?.name || '-' }}
</template>
<template #roles="{ record }">
<a-tag
v-for="role in record.roles"
:key="role.id"
color="blue"
>
<a-tag v-for="role in record.roles" :key="role.id" color="blue">
{{ role.name }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button
type="link"
size="small"
@click="handleView(record)"
>查看</a-button
>
<a-button
type="link"
size="small"
@click="handleEdit(record)"
>编辑</a-button
>
<a-button
type="link"
size="small"
@click="handleRole(record)"
>角色</a-button
>
<a-button
type="link"
size="small"
@click="handleResetPassword(record)"
>重置密码</a-button
>
<a-popconfirm
title="确定删除该用户吗?"
@confirm="handleDelete(record)"
>
<a-button type="link" size="small" danger
>删除</a-button
>
<a-button type="link" size="small" @click="handleView(record)">查看</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
<a-button type="link" size="small" @click="handleRole(record)">角色</a-button>
<a-button type="link" size="small" @click="handleResetPassword(record)">重置密码</a-button>
<a-popconfirm title="确定删除该用户吗?" @confirm="handleDelete(record)">
<a-button type="link" size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
@@ -188,119 +129,57 @@
</div>
<!-- 新增/编辑用户弹窗 -->
<save-dialog
v-if="dialog.save"
ref="saveDialogRef"
@success="handleSaveSuccess"
@closed="dialog.save = false"
/>
<save-dialog v-if="dialog.save" ref="saveDialogRef" @success="handleSaveSuccess" @closed="dialog.save = false" />
<!-- 角色设置弹窗 -->
<role-dialog
v-if="dialog.role"
ref="roleDialogRef"
@success="handleRoleSuccess"
@closed="dialog.role = false"
/>
<role-dialog v-if="dialog.role" ref="roleDialogRef" @success="handleRoleSuccess" @closed="dialog.role = false" />
<!-- 批量分配部门弹窗 -->
<department-dialog
v-if="dialog.department"
ref="departmentDialogRef"
@success="handleDepartmentSuccess"
@closed="dialog.department = false"
/>
<department-dialog v-if="dialog.department" ref="departmentDialogRef" @success="handleDepartmentSuccess" @closed="dialog.department = false" />
<!-- 批量分配角色弹窗 -->
<batch-role-dialog
v-if="dialog.batchRole"
ref="batchRoleDialogRef"
@success="handleBatchRoleSuccess"
@closed="dialog.batchRole = false"
/>
<batch-role-dialog v-if="dialog.batchRole" ref="batchRoleDialogRef" @success="handleBatchRoleSuccess" @closed="dialog.batchRole = false" />
<!-- 导入用户弹窗 -->
<sc-import
v-model:open="dialog.import"
title="导入用户"
:api="authApi.users.import.post"
:template-api="authApi.users.downloadTemplate.get"
filename="用户"
@success="handleImportSuccess"
/>
<sc-import v-model:open="dialog.import" title="导入用户" :api="authApi.user.import.post" :template-api="authApi.user.downloadTemplate.get" filename="用户" @success="handleImportSuccess" />
<!-- 导出用户弹窗 -->
<sc-export
v-model:open="dialog.export"
title="导出用户"
:api="handleExportApi"
:default-filename="`用户列表_${Date.now()}`"
:show-options="false"
tip="导出当前选中或所有用户数据"
@success="handleExportSuccess"
/>
<sc-export v-model:open="dialog.export" title="导出用户" :api="handleExportApi" :default-filename="`用户列表_${Date.now()}`" :show-options="false" tip="导出当前选中或所有用户数据" @success="handleExportSuccess" />
</template>
<script setup>
import { ref, reactive, onMounted, watch } from "vue";
import { message, Modal } from "ant-design-vue";
import {
SearchOutlined,
RedoOutlined,
PlusOutlined,
DeleteOutlined,
CheckCircleOutlined,
ApartmentOutlined,
TeamOutlined,
DownOutlined,
ImportOutlined,
ExportOutlined,
DownloadOutlined,
UserOutlined,
} from "@ant-design/icons-vue";
import scTable from "@/components/scTable/index.vue";
import scImport from "@/components/scImport/index.vue";
import scExport from "@/components/scExport/index.vue";
import saveDialog from "./components/SaveDialog.vue";
import roleDialog from "./components/RoleDialog.vue";
import departmentDialog from "./components/DepartmentDialog.vue";
import batchRoleDialog from "./components/BatchRoleDialog.vue";
import authApi from "@/api/auth";
import { useTable } from "@/hooks/useTable";
import { ref, reactive, onMounted, watch } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { SearchOutlined, RedoOutlined, PlusOutlined, DeleteOutlined, CheckCircleOutlined, ApartmentOutlined, TeamOutlined, DownOutlined, ImportOutlined, ExportOutlined, DownloadOutlined, UserOutlined } from '@ant-design/icons-vue'
import scTable from '@/components/scTable/index.vue'
import scImport from '@/components/scImport/index.vue'
import scExport from '@/components/scExport/index.vue'
import saveDialog from './components/SaveDialog.vue'
import roleDialog from './components/RoleDialog.vue'
import departmentDialog from './components/DepartmentDialog.vue'
import batchRoleDialog from './components/BatchRoleDialog.vue'
import authApi from '@/api/auth'
import { useTable } from '@/hooks/useTable'
defineOptions({
name: "authUser",
});
name: 'authUser',
})
// 使用useTable hooks
const {
tableRef,
searchForm,
tableData,
loading,
pagination,
selectedRows,
rowSelection,
handleSearch,
handleReset,
handlePaginationChange,
handleSelectChange,
handleSelectAll,
refreshTable,
} = useTable({
api: authApi.users.list.get,
const { tableRef, searchForm, tableData, loading, pagination, selectedRows, rowSelection, handleSearch, handleReset, handlePaginationChange, handleSelectChange, handleSelectAll, refreshTable } = useTable({
api: authApi.user.list.get,
searchForm: {
username: "",
real_name: "",
email: "",
phone: "",
username: '',
real_name: '',
email: '',
phone: '',
department_id: null,
status: null,
},
columns: [],
needPagination: true,
needSelection: true,
});
})
// 对话框状态
const dialog = reactive({
@@ -310,422 +189,411 @@ const dialog = reactive({
batchRole: false,
import: false,
export: false,
});
})
// 弹窗引用
const saveDialogRef = ref(null);
const roleDialogRef = ref(null);
const departmentDialogRef = ref(null);
const batchRoleDialogRef = ref(null);
const saveDialogRef = ref(null)
const roleDialogRef = ref(null)
const departmentDialogRef = ref(null)
const batchRoleDialogRef = ref(null)
// 部门树数据
const departmentTree = ref([]);
const filteredDepartmentTree = ref([]);
const selectedDeptKeys = ref([]);
const departmentKeyword = ref("");
const expandedDeptKeys = ref([]);
const departmentTree = ref([])
const filteredDepartmentTree = ref([])
const selectedDeptKeys = ref([])
const departmentKeyword = ref('')
const expandedDeptKeys = ref([])
// 行key
const rowKey = "id";
const rowKey = 'id'
// 递归获取所有部门节点的key
const getAllDepartmentKeys = (nodes) => {
const keys = [];
const keys = []
const traverse = (list) => {
list.forEach((node) => {
// 如果节点有children且不为空,则该节点需要展开
if (node.children && node.children.length > 0) {
keys.push(node.id);
traverse(node.children);
keys.push(node.id)
traverse(node.children)
}
});
};
traverse(nodes);
return keys;
};
})
}
traverse(nodes)
return keys
}
// 监听部门树数据变化,自动展开所有节点
watch(
() => filteredDepartmentTree.value,
(newData) => {
if (newData && newData.length > 0) {
expandedDeptKeys.value = getAllDepartmentKeys(newData);
expandedDeptKeys.value = getAllDepartmentKeys(newData)
} else {
expandedDeptKeys.value = [];
expandedDeptKeys.value = []
}
},
{ immediate: true, deep: true },
);
)
// 表格列配置
const columns = [
{
title: "头像",
dataIndex: "avatar",
key: "avatar",
title: '头像',
dataIndex: 'avatar',
key: 'avatar',
width: 80,
align: "center",
slot: "avatar",
align: 'center',
slot: 'avatar',
},
{ title: "用户名", dataIndex: "username", key: "username", width: 150 },
{ title: "姓名", dataIndex: "real_name", key: "real_name", width: 150 },
{ title: '用户名', dataIndex: 'username', key: 'username', width: 150 },
{ title: '姓名', dataIndex: 'real_name', key: 'real_name', width: 150 },
{
title: "邮箱",
dataIndex: "email",
key: "email",
title: '邮箱',
dataIndex: 'email',
key: 'email',
width: 180,
ellipsis: true,
},
{ title: "手机号", dataIndex: "phone", key: "phone", width: 130 },
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
{
title: "部门",
dataIndex: "department",
key: "department",
slot: "department",
title: '部门',
dataIndex: 'department',
key: 'department',
slot: 'department',
width: 150,
ellipsis: true,
},
{
title: "角色",
dataIndex: "roles",
key: "roles",
title: '角色',
dataIndex: 'roles',
key: 'roles',
width: 200,
slot: "roles",
slot: 'roles',
ellipsis: true,
},
{
title: "状态",
dataIndex: "status",
key: "status",
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
align: "center",
slot: "status",
align: 'center',
slot: 'status',
},
{
title: "最后登录",
dataIndex: "last_login_at",
key: "last_login_at",
title: '最后登录',
dataIndex: 'last_login_at',
key: 'last_login_at',
width: 180,
},
{
title: "操作",
dataIndex: "action",
key: "action",
title: '操作',
dataIndex: 'action',
key: 'action',
width: 280,
align: "center",
slot: "action",
fixed: "right",
align: 'center',
slot: 'action',
fixed: 'right',
},
];
]
// 加载部门树
const loadDepartmentTree = async () => {
try {
const res = await authApi.departments.tree.get();
const res = await authApi.department.tree.get()
if (res.code === 200) {
departmentTree.value = res.data || [];
filteredDepartmentTree.value = res.data || [];
departmentTree.value = res.data || []
filteredDepartmentTree.value = res.data || []
}
} catch (error) {
console.error("加载部门树失败:", error);
console.error('加载部门树失败:', error)
}
};
}
// 部门搜索
const handleDeptSearch = (e) => {
const keyword = e.target?.value || "";
departmentKeyword.value = keyword;
const keyword = e.target?.value || ''
departmentKeyword.value = keyword
if (!keyword) {
filteredDepartmentTree.value = departmentTree.value;
return;
filteredDepartmentTree.value = departmentTree.value
return
}
// 递归过滤部门树
const filterTree = (nodes) => {
return nodes.reduce((acc, node) => {
const isMatch =
node.name &&
node.name.toLowerCase().includes(keyword.toLowerCase());
const filteredChildren = node.children
? filterTree(node.children)
: [];
const isMatch = node.name && node.name.toLowerCase().includes(keyword.toLowerCase())
const filteredChildren = node.children ? filterTree(node.children) : []
if (isMatch || filteredChildren.length > 0) {
acc.push({
...node,
children:
filteredChildren.length > 0
? filteredChildren
: undefined,
});
children: filteredChildren.length > 0 ? filteredChildren : undefined,
})
}
return acc;
}, []);
};
return acc
}, [])
}
filteredDepartmentTree.value = filterTree(departmentTree.value);
};
filteredDepartmentTree.value = filterTree(departmentTree.value)
}
// 重置 - 覆盖useTable的handleReset以添加额外逻辑
const handleUserReset = () => {
searchForm.username = "";
searchForm.real_name = "";
searchForm.email = "";
searchForm.phone = "";
searchForm.status = null;
searchForm.department_id = null;
selectedDeptKeys.value = [];
departmentKeyword.value = "";
filteredDepartmentTree.value = departmentTree.value;
handleSearch();
};
searchForm.username = ''
searchForm.real_name = ''
searchForm.email = ''
searchForm.phone = ''
searchForm.status = null
searchForm.department_id = null
selectedDeptKeys.value = []
departmentKeyword.value = ''
filteredDepartmentTree.value = departmentTree.value
handleSearch()
}
// 部门选择事件
const onDeptSelect = (selectedKeys) => {
if (selectedKeys && selectedKeys.length > 0) {
searchForm.department_id = selectedKeys[0];
searchForm.department_id = selectedKeys[0]
} else {
searchForm.department_id = null;
searchForm.department_id = null
}
handleSearch();
};
handleSearch()
}
// 批量删除
const handleBatchDelete = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要删除的用户");
return;
message.warning('请选择要删除的用户')
return
}
Modal.confirm({
title: "确认删除",
title: '确认删除',
content: `确定删除选中的 ${selectedRows.value.length} 个用户吗?`,
okText: "删除",
okType: "danger",
cancelText: "取消",
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
try {
const ids = selectedRows.value.map((item) => item.id);
const res = await authApi.users.batchDelete.post({ ids });
const ids = selectedRows.value.map((item) => item.id)
const res = await authApi.user.batchDelete.post({ ids })
if (res.code === 200) {
message.success("删除成功");
selectedRows.value = [];
refreshTable();
message.success('删除成功')
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || "删除失败");
message.error(res.message || '删除失败')
}
} catch (error) {
console.error("批量删除用户失败:", error);
message.error("删除失败");
console.error('批量删除用户失败:', error)
message.error('删除失败')
}
},
});
};
})
}
// 批量更新状态
const handleBatchStatus = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要操作的用户");
return;
message.warning('请选择要操作的用户')
return
}
Modal.confirm({
title: "确认操作",
content: "确定要批量启用/禁用选中的用户吗?",
okText: "确定",
cancelText: "取消",
title: '确认操作',
content: '确定要批量启用/禁用选中的用户吗?',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
const ids = selectedRows.value.map((item) => item.id);
const status = selectedRows.value[0].status === 1 ? 0 : 1;
const res = await authApi.users.batchStatus.post({
const ids = selectedRows.value.map((item) => item.id)
const status = selectedRows.value[0].status === 1 ? 0 : 1
const res = await authApi.user.batchStatus.post({
ids,
status,
});
})
if (res.code === 200) {
message.success("操作成功");
selectedRows.value = [];
refreshTable();
message.success('操作成功')
selectedRows.value = []
refreshTable()
} else {
message.error(res.message || "操作失败");
message.error(res.message || '操作失败')
}
} catch (error) {
console.error("批量更新状态失败:", error);
message.error("操作失败");
console.error('批量更新状态失败:', error)
message.error('操作失败')
}
},
});
};
})
}
// 批量分配部门
const handleBatchDepartment = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要分配部门的用户");
return;
message.warning('请选择要分配部门的用户')
return
}
dialog.department = true;
dialog.department = true
setTimeout(() => {
departmentDialogRef.value?.open(
selectedRows.value.map((item) => item.id),
);
}, 0);
};
departmentDialogRef.value?.open(selectedRows.value.map((item) => item.id))
}, 0)
}
// 批量分配角色
const handleBatchRoles = () => {
if (selectedRows.value.length === 0) {
message.warning("请选择要分配角色的用户");
return;
message.warning('请选择要分配角色的用户')
return
}
dialog.batchRole = true;
dialog.batchRole = true
setTimeout(() => {
batchRoleDialogRef.value?.open(
selectedRows.value.map((item) => item.id),
);
}, 0);
};
batchRoleDialogRef.value?.open(selectedRows.value.map((item) => item.id))
}, 0)
}
// 导出数据
const handleExport = () => {
dialog.export = true;
};
dialog.export = true
}
// 导出API封装
const handleExportApi = async () => {
const ids = selectedRows.value.map((item) => item.id);
return await authApi.users.export.post({
const ids = selectedRows.value.map((item) => item.id)
return await authApi.user.export.post({
ids: ids.length > 0 ? ids : undefined,
});
};
})
}
// 导出成功回调
const handleExportSuccess = () => {
selectedRows.value = [];
};
selectedRows.value = []
}
// 导入用户
const handleImport = () => {
dialog.import = true;
};
dialog.import = true
}
// 导入成功回调
const handleImportSuccess = () => {
refreshTable();
};
refreshTable()
}
// 下载模板
const handleDownloadTemplate = async () => {
try {
const blob = await authApi.users.downloadTemplate.get();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "用户导入模板.xlsx";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
message.success("下载成功");
const blob = await authApi.user.downloadTemplate.get()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '用户导入模板.xlsx'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
message.success('下载成功')
} catch (error) {
console.error("下载模板失败:", error);
message.error("下载失败");
console.error('下载模板失败:', error)
message.error('下载失败')
}
};
}
// 重置密码
const handleResetPassword = (record) => {
Modal.confirm({
title: "重置密码",
content: "确定要重置该用户的密码吗?重置后密码为: 123456",
okText: "确定",
cancelText: "取消",
title: '重置密码',
content: '确定要重置该用户的密码吗?重置后密码为: 123456',
okText: '确定',
cancelText: '取消',
onOk: async () => {
try {
// TODO: 实现重置密码接口
message.success("密码重置成功");
message.success('密码重置成功')
} catch (error) {
console.error("重置密码失败:", error);
message.error("重置密码失败");
console.error('重置密码失败:', error)
message.error('重置密码失败')
}
},
});
};
})
}
// 新增用户
const handleAdd = () => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("add");
}, 0);
};
saveDialogRef.value?.open('add')
}, 0)
}
// 查看用户
const handleView = (record) => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("show").setData(record);
}, 0);
};
saveDialogRef.value?.open('show').setData(record)
}, 0)
}
// 编辑用户
const handleEdit = (record) => {
dialog.save = true;
dialog.save = true
setTimeout(() => {
saveDialogRef.value?.open("edit").setData(record);
}, 0);
};
saveDialogRef.value?.open('edit').setData(record)
}, 0)
}
// 设置角色
const handleRole = (record) => {
dialog.role = true;
dialog.role = true
setTimeout(() => {
if (roleDialogRef.value) {
roleDialogRef.value.open();
roleDialogRef.value.setData(record);
roleDialogRef.value.open()
roleDialogRef.value.setData(record)
}
}, 0);
};
}, 0)
}
// 删除用户
const handleDelete = async (record) => {
try {
const res = await authApi.users.delete.delete(record.id);
const res = await authApi.user.delete.delete(record.id)
if (res.code === 200) {
message.success("删除成功");
refreshTable();
message.success('删除成功')
refreshTable()
} else {
message.error(res.message || "删除失败");
message.error(res.message || '删除失败')
}
} catch (error) {
console.error("删除用户失败:", error);
message.error("删除失败");
console.error('删除用户失败:', error)
message.error('删除失败')
}
};
}
// 保存成功回调
const handleSaveSuccess = () => {
refreshTable();
};
refreshTable()
}
// 角色设置成功回调
const handleRoleSuccess = () => {
refreshTable();
};
refreshTable()
}
// 批量分配部门成功回调
const handleDepartmentSuccess = () => {
selectedRows.value = [];
refreshTable();
};
selectedRows.value = []
refreshTable()
}
// 批量分配角色成功回调
const handleBatchRoleSuccess = () => {
selectedRows.value = [];
refreshTable();
};
selectedRows.value = []
refreshTable()
}
// 初始化
onMounted(() => {
loadDepartmentTree();
});
loadDepartmentTree()
})
</script>