前端代码格式化
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user