开源准备

This commit is contained in:
zhangtao
2024-12-10 17:39:26 +08:00
commit cbaf324e19
315 changed files with 39314 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
<template>
<a-layout>
<!-- 页面主体 -->
<div class="container">
<a-layout-content :style="contentStyle">
<div class="header">
<div class="logo">
<a-image src="/logo.png" :preview="false" />
</div>
<div class="title">FastAPI Vue Admin</div>
</div>
<div class="desc">FastAPI Vue Admin 是完全开源的权限管理系统</div>
<div class="login-main" style="width: 330px; margin: 0 auto;">
<a-tabs centered>
<a-tab-pane :key="1" tab="账户密码登录">
<a-form :model="loginForm" @finish="onFinish">
<a-form-item name="username" :rules="[{ required: true, message: '用户名是必填项!' }]">
<a-input v-model:value="loginForm.username" placeholder="用户名: admin or test or demo">
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item name="password" :rules="[{ required: true, message: '密码是必填项!' }]">
<a-input-password v-model:value="loginForm.password" placeholder="密码: gitee 或 github 查看">
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item v-if="captchaState.enable" name="captcha" :rules="[{ required: true, message: '验证码是必填项!' }]">
<a-input v-model:value="loginForm.captcha" placeholder="验证码">
<template #addonAfter>
<div class="login-form-captcha" @click="requestCaptcha">
<a-image :src="captchaState.img_base" :preview="false" />
</div>
</template>
</a-input>
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="loginForm.remember">自动登录</a-checkbox>
<a class="login-form-forgot" @click="showModal('forgetPassword')">忘记密码</a>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" class="login-form-button" :loading="loginFlag">
登录
</a-button>
<div class="register-link">
还没有账号? <a @click="showModal('register')">立即注册</a>
</div>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</div>
</a-layout-content>
<!-- 页面底部 -->
<a-layout-footer :style="footerStyle">
<div class="footer-copyright">
<a-button type="link" href="https://gitee.com/tao__tao/fastapi_vue_admin.git">
<icon-font type="icon-copyright" :style="{ fontSize: '16px' }" />
Copyright © 2024 insistence.tech All Rights Reserved. 皖ICP备2023021369号-1
</a-button>
</div>
</a-layout-footer>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal
v-model:open="modalVisible"
:title="modalType === 'forgetPassword' ? '忘记密码' : '用户注册'"
@ok="handleModalSubmit"
:confirmLoading="modalLoading">
<!-- 忘记密码表单 -->
<a-form v-if="modalType === 'forgetPassword'"
:model="forgetPasswordForm"
ref="forgetPasswordFormRef"
:rules="{
username: [{ required: true, message: '请输入用户名!' }],
mobile: [{ required: true, message: '请输入手机号!', pattern: /^1[3-9]\d{9}$/ }],
new_password: [{ required: true, message: '请输入新密码!', min: 6 }]
}">
<a-form-item label="用户名" name="username">
<a-input v-model:value="forgetPasswordForm.username" placeholder="请输入用户名" />
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input v-model:value="forgetPasswordForm.mobile" placeholder="请输入手机号" />
</a-form-item>
<a-form-item label="新密码" name="new_password">
<a-input-password v-model:value="forgetPasswordForm.new_password" placeholder="请输入新密码" />
</a-form-item>
</a-form>
<!-- 注册表单 -->
<a-form v-else
:model="registerForm"
ref="registerFormRef"
:rules="{
username: [{ required: true, message: '请输入用户名!', min: 3 }],
name: [{ required: true, message: '请输入名称!' }],
password: [{ required: true, message: '请输入密码!', min: 6 }],
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号!' }],
email: [{ type: 'email', message: '请输入正确的邮箱格式!' }]
}">
<a-form-item label="用户名" name="username">
<a-input v-model:value="registerForm.username" placeholder="请输入用户名" />
</a-form-item>
<a-form-item label="名称" name="name">
<a-input v-model:value="registerForm.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password v-model:value="registerForm.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="手机号" name="mobile">
<a-input v-model:value="registerForm.mobile" placeholder="请输入手机号" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="registerForm.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="性别" name="gender">
<a-radio-group v-model:value="registerForm.gender">
<a-radio :value="1">男</a-radio>
<a-radio :value="2">女</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</a-layout>
</template>
<script lang="ts" setup>
import type { CSSProperties } from "vue";
import { ref, reactive, onMounted } from "vue";
import { useRouter } from "vue-router";
import { UserOutlined, LockOutlined, createFromIconfontCN } from '@ant-design/icons-vue';
import { save_token } from "@/utils/util"
import { message } from 'ant-design-vue';
import { login, getCaptcha } from "@/api/system/auth"
import { registerUser, forgetPassword } from "@/api/system/user"
import type { LoginForm, CaptchaState, ForgetPasswordForm, RegisterForm } from './types'
const router = useRouter();
const loginFlag = ref(false);
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js',
});
const contentStyle: CSSProperties = {
minHeight: 900,
height: "900px",
background: "none",
padding: "200px 0",
};
const footerStyle: CSSProperties = {
textAlign: "center",
background: "none",
};
// 统一的弹窗控制
const modalVisible = ref(false);
const modalLoading = ref(false);
const modalType = ref<'forgetPassword' | 'register'>('forgetPassword');
// 忘记密码相关
const forgetPasswordFormRef = ref();
const forgetPasswordForm = reactive<ForgetPasswordForm>({
username: '',
mobile: '',
new_password: ''
});
// 注册相关
const registerFormRef = ref();
const registerForm = reactive<RegisterForm>({
username: '',
name: '',
password: '',
mobile: '',
email: '',
gender: 1
});
const loginForm = reactive<LoginForm>({
username: "",
password: "",
captcha: "",
captcha_key: "",
remember: true
});
const captchaState = reactive<CaptchaState>({
enable: true,
key: "",
img_base: ""
});
const showModal = (type: 'forgetPassword' | 'register') => {
modalType.value = type;
modalVisible.value = true;
};
const handleModalSubmit = () => {
modalLoading.value = true;
if (modalType.value === 'forgetPassword') {
forgetPasswordFormRef.value.validate().then(() => {
forgetPassword(forgetPasswordForm)
.then(response => {
if (response.data.status_code === 200) {
message.success('密码重置成功');
modalVisible.value = false;
}
})
.finally(() => modalLoading.value = false);
});
} else {
registerFormRef.value.validate().then(() => {
registerUser({ ...registerForm })
.then(response => {
if (response.data.status_code === 200) {
message.success('注册成功');
modalVisible.value = false;
}
})
.finally(() => modalLoading.value = false);
});
}
};
const onFinish = (values: LoginForm) => {
loginFlag.value = true;
values.captcha_key = captchaState.key;
login(values)
.then(response => {
const { status_code, data } = response.data;
if (status_code === 200) {
save_token(data.access_token, data.refresh_token, data.expires_in);
router.push('/');
}
})
.catch(error => {
if (error.response?.data?.status_code === 500) {
requestCaptcha();
}
})
.finally(() => loginFlag.value = false);
};
const requestCaptcha = () => {
getCaptcha()
.then(response => {
const { status_code, data } = response.data;
if (status_code === 200) {
captchaState.key = data.key;
captchaState.img_base = data.img_base;
} else {
captchaState.enable = false;
}
})
.catch(() => captchaState.enable = false);
};
onMounted(requestCaptcha);
</script>
<style lang="scss" scoped>
.ant-btn-link {
color: rgba(0, 0, 0, 0.65);
margin-inline-end: 8px;
padding: 0;
}
.container {
background-image: url("/background.png");
background-size: 100% 100%;
.desc {
text-align: center;
font-size: 15px;
margin: 12px 0 40px;
}
.header {
display: flex;
line-height: 44px;
justify-content: center;
align-items: center;
.logo {
width: 44px;
height: 44px;
margin-inline-end: 16px;
}
.title {
font-size: 33px;
font-weight: 650;
}
}
}
.login-form {
&-button {
width: 100%;
}
&-captcha {
width: 80px;
cursor: pointer;
}
&-forgot {
float: right;
color: #1890ff;
cursor: pointer;
}
}
.register-link {
text-align: center;
margin-top: 16px;
a {
color: #1890ff;
cursor: pointer;
}
}
:deep(.ant-input-group .ant-input-group-addon) {
padding: 0;
}
</style>
+28
View File
@@ -0,0 +1,28 @@
export interface LoginForm {
username: string;
password: string;
captcha: string;
captcha_key: string;
remember: boolean;
}
export interface CaptchaState {
enable: boolean;
key: string;
img_base: string;
}
export interface ForgetPasswordForm {
username: string;
mobile: string;
new_password: string;
}
export interface RegisterForm {
username: string;
name: string;
password: string;
mobile: string;
email: string;
gender: number;
}
+513
View File
@@ -0,0 +1,513 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="tree-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear />
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-space>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button @click="resetFields">重置</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card
title="部门列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }"
>
<template #extra>
<a-space>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')">新建</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="handleMoreClick">
<a-menu-item key="1"><CheckOutlined />批量启用</a-menu-item>
<a-menu-item key="2"><StopOutlined />批量停用</a-menu-item>
</a-menu>
</template>
<a-button>更多<DownOutlined /></a-button>
</a-dropdown>
</a-space>
</template>
<a-table
:rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:loading="tableLoading"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:row-selection="rowSelection"
:pagination="false"
:style="{ minHeight: '500px' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'name'">
<span :style="{ color: !record.available ? 'rgb(255, 77, 79)' : 'rgba(0, 0, 0, .88)' }">
{{ record.name }}
</span>
</template>
<template v-else-if="column.dataIndex === 'available'">
<a-badge :status="record.available ? 'success' : 'error'" />
{{ record.available ? '启用' : '停用' }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-space size="middle">
<a @click="modalHandle('view', record)">查看</a>
<a @click="modalHandle('update', record)">修改</a>
<a-popconfirm title="确定要删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal
v-model:open="openModal"
@ok="handleModalSumbit"
:width="800"
:destroyOnClose="true"
:confirmLoading="modalSubmitLoading"
style="top: 30px"
>
<template #title>
<span>{{ modalTitle === 'create' ? '新建部门' : (modalTitle === 'view' ? '查看部门' : '修改部门') }}</span>
</template>
<!-- 查看表单 -->
<template v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions
:column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }"
bordered
>
<a-descriptions-item label="名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="排序">{{ detailState.order }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :color="detailState.available ? 'green' : 'red'" />
{{ detailState.available ? '启用' : '禁用' }}
</a-descriptions-item>
<a-descriptions-item label="上级部门" :span="2">{{ detailState.parent_name }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</template>
<!-- 新建表单 -->
<template v-else-if="modalTitle === 'create'">
<a-form
ref="createForm"
:model="createState"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 15 }"
>
<a-form-item
name="name"
label="名称"
:rules="[{ required: true, message: '请输入名称' }]"
>
<a-input v-model:value="createState.name" placeholder="请输入名称" allowClear />
</a-form-item>
<a-form-item
name="order"
label="排序"
:rules="[{ required: true, message: '请输入排序' }]"
>
<a-input-number v-model:value="createState.order" :min="1" />
</a-form-item>
<a-form-item name="parent_id" label="上级部门">
<a-tree-select
v-model:value="createState.parent_id"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="dataSource"
:field-names="{ children: 'children', label: 'name', value: 'id' }"
tree-node-filter-prop="name"
style="width: 100%"
show-search
allow-clear
placeholder="请选择上级部门"
/>
</a-form-item>
<a-form-item
name="available"
label="状态"
:rules="[{ required: true, message: '请选择状态' }]"
>
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea
v-model:value="createState.description"
placeholder="请输入备注"
:rows="4"
allowClear
/>
</a-form-item>
</a-form>
</template>
<!-- 修改表单 -->
<template v-else>
<a-form
ref="updateForm"
:model="updateState"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 15 }"
>
<a-form-item
name="name"
label="名称"
:rules="[{ required: true, message: '请输入名称' }]"
>
<a-input v-model:value="updateState.name" placeholder="请输入名称" allowClear />
</a-form-item>
<a-form-item
name="order"
label="排序"
:rules="[{ required: true, message: '请输入排序' }]"
>
<a-input-number v-model:value="updateState.order" :min="1" />
</a-form-item>
<a-form-item name="parent_id" label="上级部门">
<a-tree-select
v-model:value="updateState.parent_id"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
:tree-data="dataSource"
:field-names="{ children: 'children', label: 'name', value: 'id' }"
tree-node-filter-prop="name"
style="width: 100%"
show-search
allow-clear
placeholder="请选择上级部门"
/>
</a-form-item>
<a-form-item
name="available"
label="状态"
:rules="[{ required: true, message: '请选择状态' }]"
>
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea
v-model:value="updateState.description"
placeholder="请输入备注"
:rows="4"
allowClear
/>
</a-form-item>
</a-form>
</template>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted, h, unref } from 'vue';
import { message, Modal, Table } from 'ant-design-vue';
import type { MenuProps, TableColumnsType } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { listToTree, cloneDeep, isEmpty } from '@/utils/util';
import { getDeptList, createDept, updateDept, deleteDept, batchAvailableDept } from '@/api/system/dept';
import PageHeader from '@/components/PageHeader.vue'
import type { searchDataType, tableDataType } from './types';
// 响应式数据
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({
name: null,
available: null
});
const createState = reactive<tableDataType>({
name: '',
order: 1,
available: true,
parent_id: undefined,
description: ''
});
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
order: 1,
available: true,
parent_id: undefined,
description: ''
});
const detailState = ref<tableDataType>({});
const columns = reactive<TableColumnsType>([
{
title: '部门名称',
dataIndex: 'name',
align: 'center',
key: 'name',
width: 160
},
{
title: '排序',
dataIndex: 'order',
key: 'order',
align: 'center',
width: 100
},
{
title: '状态',
dataIndex: 'available',
key: 'available',
align: 'center',
width: 100
},
{
title: '备注',
dataIndex: 'description',
key: 'description',
align: 'center'
},
{
title: '创建时间',
dataIndex: 'created_at',
align: 'center',
key: 'created_at'
},
{
title: '更新日期',
dataIndex: 'updated_at',
align: 'center',
key: 'created_at'
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
align: 'center',
width: 150
}
]);
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
loadingData();
};
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name;
}
if (queryState.available) {
params['available'] = queryState.available === "true" ? true : false;
}
getDeptList(params).then(response => {
const result = response.data;
dataSource.value = listToTree(result.data.items);
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
queryState.available = null;
loadingData();
};
// 删除
const deleteRow = (row: tableDataType) => {
deleteDept({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error);
});
};
// 批量启用/停用
const handleMoreClick: MenuProps['onClick'] = e => {
if (!selectedRowKeys.value || !(selectedRowKeys.value.length > 0)) {
message.warning('请先勾选数据');
return;
}
Modal.confirm({
title: '提示',
content: e.key == 1 ? '是否确定启用选择项?' : '是否确定停用选择项?',
onOk() {
const body = { ids: selectedRowKeys.value, available: e.key == 1 ? true : false };
batchAvailableDept(body).then(response => {
const result = response.data;
message.success(result.msg);
selectedRowKeys.value = [];
loadingData();
}).catch(error => {
console.log(error);
});
}
});
};
// 弹窗关键字处理
const modalHandle = (modalType: string, record?: tableDataType) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && record !== undefined) {
detailStateLoading.value = true;
detailState.value = record
detailStateLoading.value = false;
} else if (modalType === 'update' && record !== undefined) {
Object.keys(updateState).forEach(key => {
updateState[key] = record[key];
})
}
};
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
});
createDept(createBody).then(response => {
const result = response.data;
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
createState.order = 1;
message.success(result.msg);
loadingData();
}).catch(error => {
console.error(error);
});
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateDept(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
});
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
}
};
</script>
<style lang="scss" scoped>
.tree-search-wrapper {
margin-block-end: 16px;
}
</style>
+25
View File
@@ -0,0 +1,25 @@
export interface searchDataType {
name?: string
available?: string
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
order?: number;
parent_id?: number;
parent_name?: string;
available?: boolean;
description?: string;
children?: tableDataType[];
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
@@ -0,0 +1,211 @@
<template>
<a-modal title="选择创建人" v-model:open="openModal" :width="1200" :destroyOnClose="true" style="top: 30px">
<template #footer>
<a-button @click="handleModalCancel">取消</a-button>
<a-button @click="handleModalClear">清空</a-button>
<a-button type="primary" @click="handleModalSumbit">确定</a-button>
</template>
<div class="table-search-wrapper">
<a-card :bordered="true">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="creator_name" label="姓名" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入姓名" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<div class="table-wrapper">
<a-card title="用户列表" :bordered="true" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px' }">
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource" :loading="tableLoading"
:row-selection="rowSelection" @change="handleTableChange" :scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:pagination="pagination">
<template v-slot:bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'available'">
<span><a-badge :color="record.available ? 'green' : 'red'" /> {{ record.available ? '启用' : '禁用' }}
</span>
</template>
</template>
</a-table>
</a-card>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref } from 'vue';
import type { TableColumnsType } from 'ant-design-vue';
import { getUserList } from '@/api/system/user'
import type { searchCreatorDataType, creatorTableDataType } from './types'
const openModal = ref<boolean>(false);
const tableLoading = ref(false);
const queryState: searchCreatorDataType = reactive({
name: "",
available: undefined,
});
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '姓名',
dataIndex: 'name',
align: 'center'
},
{
title: '状态',
dataIndex: 'available',
align: 'center'
},
{
title: '备注',
dataIndex: 'description',
align: 'center',
ellipsis: true,
width: 500
}
]
const dataSource = ref<creatorTableDataType[]>([]);
const loadingData = () => {
tableLoading.value = true;
dataSource.value = [];
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.available) {
params['available'] = queryState.available == "true" ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getUserList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
tableLoading.value = false;
}).catch(error => {
tableLoading.value = false;
})
}
const onFinish = () => {
pagination.current = 1;
loadingData();
}
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
queryState.available = "true"
pagination.current = 1;
loadingData();
}
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
})
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
const selectedRowKeys = ref<creatorTableDataType['id'][]>([]);
const selectedRowName = ref<creatorTableDataType['name']>('');
const onSelectChange = (selectingRowKeys: creatorTableDataType['id'][], selectingRows: creatorTableDataType[]) => {
selectedRowKeys.value = selectingRowKeys;
selectedRowName.value = selectingRows[0].name;
}
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: onSelectChange,
hideDefaultSelections: true,
type: 'radio'
}
});
const emit = defineEmits(['event']);
const handleModalCancel = () => {
openModal.value = false;
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
queryState.available = "true"
pagination.current = 1;
pagination.pageSize = pagination.defaultPageSize;
selectedRowKeys.value = [];
selectedRowName.value = undefined;
}
const handleModalClear = () => {
handleModalCancel();
emit('event', selectedRowKeys.value, selectedRowName.value);
}
const handleModalSumbit = () => {
emit('event', selectedRowKeys.value, selectedRowName.value);
handleModalCancel();
}
defineExpose({
openModal,
selectedRowKeys,
selectedRowName,
loadingData
});
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+378
View File
@@ -0,0 +1,378 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="request_path" label="请求路径" style="max-width: 300px;">
<a-input v-model:value="queryState.request_path" placeholder="请输入请求路径" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="creator" label="创建人" style="max-width: 300px;">
<a-select v-model:value="queryState.creator_name" placeholder="请选择创建人" :open="false"
@click="selectModalHandle">
<template #suffixIcon>
<search-outlined />
</template>
</a-select>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="date-range-picker" label="创建日期" style="max-width: 350px;">
<a-range-picker v-model:value="queryState.date_range" value-format="YYYY-MM-DD" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="日志列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(DownOutlined)" @click="handleExport"
style="margin-right: 10px;">
导出
</a-button>
</template>
<a-table
:rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:loading="tableLoading"
@change="handleTableChange"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:pagination="pagination"
:style="{ minHeight: '500px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'request_method'">
<a-tag :color="getRequestMethodColor(record.request_method)">{{ record.request_method }}</a-tag>
</template>
<template v-if="column.dataIndex === 'response_code'">
<a-tag :color="record.response_code === 200 ? 'green' : 'red'">{{ record.response_code }}</a-tag>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a v-on:click="modalHandle('view', record, index)">查看</a>
<a-popconfirm title="确定要删除吗?" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="openModal = false" :width="800" :destroyOnClose="true" style="top: 30px">
<template #title>
<span>查看日志</span>
</template>
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }" :labelStyle="{ width: '140px' }"
bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize + detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="请求地址" :span="2">{{ detailState.request_path }}</a-descriptions-item>
<a-descriptions-item label="请求方法">
<a-tag :color="getRequestMethodColor(detailState.request_method)">{{ detailState.request_method }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="IP地址">{{ detailState.request_ip }}</a-descriptions-item>
<a-descriptions-item label="浏览器">{{ detailState.request_browser }}</a-descriptions-item>
<a-descriptions-item label="系统">{{ detailState.request_os }}</a-descriptions-item>
<a-descriptions-item label="响应码" :span="4">
<a-tag :color="detailState.response_code === 200 ? 'green' : 'red'">{{ detailState.response_code
}}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="请求体" :span="4">{{ detailState.request_payload }}</a-descriptions-item>
<a-descriptions-item label="返回信息" :span="4">
<div class="scrollable-content">{{ detailState.response_json }}</div>
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</a-modal>
</div>
<!-- 选择人弹窗 -->
<SelectorModal ref="selectorModal" @event="handleSelectorModalEvent" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, h } from 'vue';
import type { TableColumnsType } from 'ant-design-vue';
import { SearchOutlined, DownOutlined} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import type { searchDataType, tableDataType, creatorType } from './types'
import PageHeader from '@/components/PageHeader.vue';
import { getLogList, deleteLog, exportLog } from '@/api/system/log'
import SelectorModal from './SelectorModal.vue'
import XLSX from 'xlsx';
const tableLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const detailStateLoading = ref(false);
const openModal = ref(false);
const selectorModal = ref();
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
});
const queryState = reactive<searchDataType>({});
const detailState = ref<tableDataType>({});
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '请求地址',
dataIndex: 'request_path',
align: 'center',
width: 200
},
{
title: '请求方法',
dataIndex: 'request_method',
align: 'center',
width: 80
},
{
title: 'IP地址',
dataIndex: 'request_ip',
align: 'center',
width: 100
},
{
title: '浏览器',
dataIndex: 'request_browser',
align: 'center',
width: 100
},
{
title: '系统',
dataIndex: 'request_os',
align: 'center',
width: 150
},
{
title: '响应码',
dataIndex: 'response_code',
align: 'center',
width: 80
},
{
title: '描述',
dataIndex: 'description',
align: 'center',
ellipsis: true,
// width: 500
},
{
title: '创建日期',
dataIndex: 'created_at',
align: 'center',
ellipsis: true,
width: 180
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
width: 150
}
];
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.request_path) {
params['request_path'] = queryState.request_path
}
if (queryState.creator) {
params['creator'] = queryState.creator
}
if (queryState.date_range) {
params['start_time'] = `${queryState.date_range[0]} 00:00:00`;
params['end_time'] = `${queryState.date_range[1]} 23:59:59`;
}
params['page_no'] = pagination.current;
params['page_size'] = pagination.pageSize;
getLogList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
};
// 获取请求方法颜色
const getRequestMethodColor = (method: string) => {
const methodColors = {
GET: 'green',
POST: 'blue',
PUT: 'orange',
DELETE: 'red',
PATCH: 'purple',
HEAD: 'gray',
OPTIONS: 'cyan',
};
return methodColors[method] || 'black'; // 默认颜色为黑色
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
loadingData();
};
// 删除
const deleteRow = (row: tableDataType) => {
deleteLog({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
};
// 表格分页
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
};
// 查看
const modalHandle = (modalType: string, record?: tableDataType, index?: number) => {
if (modalType === 'view' && record !== undefined) {
openModal.value = true;
detailStateLoading.value = true;
detailState.value = record;
detailState.value.index = index;
detailStateLoading.value = false;
}
};
// 选择人弹窗
const selectModalHandle = () => {
selectorModal.value.openModal = true;
selectorModal.value.selectedRowKeys = [queryState.creator];
selectorModal.value.selectedRowName = queryState.creator_name;
selectorModal.value.loadingData();
};
// 选择人弹窗事件
const handleSelectorModalEvent = (selectedSelectorRowKeys?: creatorType['id'][], selectedSelectorRowName?: creatorType['name']) => {
const creator = selectedSelectorRowKeys.length ? selectedSelectorRowKeys[0] : undefined;
const creator_name = selectedSelectorRowName || undefined;
queryState.creator = creator;
queryState.creator_name = creator_name;
};
/** 导出按钮操作 */
const handleExport = () => {
// 构建查询参数
const params = {
...queryState,
page_no: 1,
page_size: pagination.total // 导出所有数据
};
// 调用 exportLog 接口
exportLog(params).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.ms-excel' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `log_${new Date().getTime()}.xlsx`; // 设置下载文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
message.success('导出成功');
}).catch(error => {
console.error('导出失败:', error);
message.error('导出失败');
});
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
.scrollable-content {
max-height: 200px;
/* 设置最大高度 */
overflow-y: auto;
/* 添加垂直滚动条 */
white-space: pre-wrap;
/* 保留换行符并允许文本换行 */
}
.json-viewer {
max-height: 300px;
overflow-y: auto;
}
</style>
+34
View File
@@ -0,0 +1,34 @@
export interface searchDataType {
request_path?: string;
creator?: number;
creator_name?: string;
date_range?: [string, string];
}
export interface tableDataType {
id?: number;
index?: number;
request_path?: string;
request_method?: string;
request_ip?: string;
request_browser?: string;
request_os?: string;
response_code?: number;
request_payload?: string;
response_json?: string;
description?: string;
creator?: creatorType;
created_at?: string;
updated_at?: string;
}
export interface searchCreatorDataType {
name?: string;
available?: string;
}
export interface creatorType {
id?: number;
name?: string;
username?: string;
}
+725
View File
@@ -0,0 +1,725 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="菜单列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">新建</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="handleMoreClick">
<a-menu-item key="1"><span style="margin-right: 10px;">
<CheckOutlined />
</span><span>批量启用</span></a-menu-item>
<a-menu-item key="2"><span style="margin-right: 10px;">
<StopOutlined />
</span><span>批量停用</span></a-menu-item>
</a-menu>
</template>
<a-button>更多
<DownOutlined />
</a-button>
</a-dropdown>
</template>
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:row-selection="rowSelection"
:loading="tableLoading"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:pagination="false"
:style="{ minHeight: '500px' }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'name'">
<span :style="{ color: !record.available ? 'rgb(255, 77, 79)' : 'rgba(0, 0, 0, .88)' }">
{{ record.name }}
</span>
</template>
<template v-if="column.dataIndex === 'type'">
<a-tag :color="record.type === 1 ? 'blue' : (record.type === 2 ? 'green' : 'orange')">
{{ record.type === 1 ? '目录' : (record.type === 2 ? '功能' : '权限') }}
</a-tag>
</template>
<template v-if="column.dataIndex === 'available'">
<span><a-badge :color="record.available ? 'green' : 'red'" /> {{ record.available ? '启用' : '禁用' }}
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', record)">查看</a>
<a @click="modalHandle('update', record)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal
v-model:open="openModal"
@ok="handleModalSumbit"
:width="800"
:destroyOnClose="true"
:confirmLoading="modalSubmitLoading"
style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建菜单' : (modalTitle === 'view' ? '查看菜单' : '修改菜单') }}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }" :labelStyle="{ width: '140px' }"
bordered>
<a-descriptions-item label="菜单名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="菜单类型">
<a-tag :color="detailState.type === 1 ? 'blue' : (detailState.type === 2 ? 'green' : 'orange')">
{{ detailState.type === 1 ? '目录' : (detailState.type === 2 ? '功能' : '权限') }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="显示排序">{{ detailState.order }}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type !== 3" label="图标">
<a-button type="text" size="small" :icon="h(icons[detailState.icon])">{{ detailState.icon }}</a-button>
</a-descriptions-item>
<a-descriptions-item label="父级菜单" :span="2">{{ detailState.parent_name }}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type !== 1" label="权限标识" :span="2">{{ detailState.permission
}}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type !== 3" label="路由名称">{{ detailState.route_name
}}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type !== 3" label="路由路径">{{ detailState.route_path
}}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type === 1" label="重定向">{{ detailState.redirect
}}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type === 2" label="组件地址">{{ detailState.component_path
}}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type !== 3" label="是否缓存">{{ detailState.cache ? '是' : '否'
}}</a-descriptions-item>
<a-descriptions-item v-if="detailState.type !== 3" label="是否隐藏">{{ detailState.hidden ? '是' : '否'
}}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :color="detailState.available ? 'green' : 'red'" />{{ detailState.available ? '启用' : '禁用' }}
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="名称" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="type" label="类型" :rules="[{ required: true, message: '请选择类型' }]">
<a-radio-group v-model:value="createState.type">
<a-radio :value="1">目录</a-radio>
<a-radio :value="2">功能</a-radio>
<a-radio :value="3">权限</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="createState.type !== 1" name="permission" label="权限标识"
:rules="[{ required: createState.type !== 1 ? true : false, message: '请输入权限标识' }]">
<a-input v-model:value="createState.permission" placeholder="请输入权限标识" allowClear></a-input>
</a-form-item>
<a-form-item name="parent_id" label="父级菜单">
<a-tree-select v-model:value="createState.parent_id"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" :tree-data="menuSelectorTreeData"
:field-names="{ children: 'children', label: 'name', value: 'id' }" tree-node-filter-prop="name"
style="width: 100%" show-search allow-clear placeholder="请选择父级菜单"></a-tree-select>
</a-form-item>
<a-form-item name="icon" label="图标">
<a-popover placement="right" trigger="click">
<template #content>
<div class="icon-clear-btn-wrapper">
<a-button :icon="h(ClearOutlined)" @click="iconClearClickHandle" danger>清空</a-button>
</div>
<a-form-item-rest>
<a-input v-model:value="iconSelector.search" placeholder="搜索图标" allowClear />
</a-form-item-rest>
<a-tabs v-model:activeKey="iconSelector.activeTab" @change="iconTabHandleChange" tabPosition="left"
style="margin-top: 20px;">
<a-tab-pane v-for="(item, index) in iconDataSource" :key="index" :tab="item.type">
<div class="icon-wrapper">
<a-flex wrap="wrap" gap="small">
<a-button
v-for="item in iconData.slice((pagination.current - 1) * pagination.pageSize, pagination.current * pagination.pageSize)"
:icon="item.icon" @click="iconHandleClick(item)"
:class="createState.icon === item.name ? 'active' : ''" />
</a-flex>
</div>
</a-tab-pane>
</a-tabs>
<div class="icon-pagination-wrapper">
<a-pagination size="small" v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize" :total="pagination.total" :showTotal="pagination.showTotal"
:showQuickJumper="pagination.showQuickJumper" :showSizeChanger="false" />
</div>
</template>
<a-button :icon="createState.icon ? h(icons[createState.icon]) : ''" style="width: 250px;">{{
createState.icon ? createState.icon : '请选择图标' }}</a-button>
</a-popover>
</a-form-item>
<a-form-item name="order" label="排序">
<a-input-number v-model:value="createState.order" :min="1" />
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
<div v-if="createState.type !== 3">
<a-divider style="font-weight: 700">以下均为前端配置项</a-divider>
<a-form-item name="route_name" label="路由名称"
:rules="[{ required: createState.type !== 3 ? true : false, message: '请输入路由名称' }]">
<a-input v-model:value="createState.route_name" placeholder="请输入路由名称" allowClear></a-input>
</a-form-item>
<a-form-item name="route_path" label="路由路径"
:rules="[{ required: createState.type !== 3 ? true : false, message: '请输入路由路径' }]">
<a-input v-model:value="createState.route_path" placeholder="请输入路由路径" allowClear></a-input>
</a-form-item>
<a-form-item v-if="createState.type === 1" name="redirect" label="重定向"
:rules="[{ required: createState.type === 1 ? true : false, message: '请输入重定向' }]">
<a-input v-model:value="createState.redirect" placeholder="请输入重定向" allowClear></a-input>
</a-form-item>
<a-form-item v-if="createState.type === 2" name="component_path" label="组件地址"
:rules="[{ required: createState.type === 2 ? true : false, message: '请输入组件地址' }]">
<a-input v-model:value="createState.component_path" placeholder="请输入组件地址" allowClear></a-input>
</a-form-item>
<a-form-item name="cache" label="是否缓存"
:rules="[{ required: createState.type !== 3 ? true : false, message: '请选择缓存状态' }]">
<a-switch v-model:checked="createState.cache"></a-switch>
</a-form-item>
<a-form-item name="hidden" label="是否隐藏"
:rules="[{ required: createState.type !== 3 ? true : false, message: '请选择隐藏状态' }]">
<a-switch v-model:checked="createState.hidden"></a-switch>
</a-form-item>
</div>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="名称" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item v-if="updateState.type !== 1" name="permission" label="权限标识"
:rules="[{ required: updateState.type !== 1 ? true : false, message: '请输入权限标识' }]">
<a-input v-model:value="updateState.permission" placeholder="请输入权限标识" allowClear></a-input>
</a-form-item>
<a-form-item name="parent_id" label="父级菜单">
<a-tree-select v-model:value="updateState.parent_id"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" :tree-data="menuSelectorTreeData"
:field-names="{ children: 'children', label: 'name', value: 'id' }" tree-node-filter-prop="name"
style="width: 100%" show-search allow-clear placeholder="请选择父级菜单"></a-tree-select>
</a-form-item>
<a-form-item name="icon" label="图标">
<a-popover placement="right" trigger="click">
<template #content>
<div class="icon-clear-btn-wrapper">
<a-button :icon="h(ClearOutlined)" @click="iconClearClickHandle" danger>清空</a-button>
</div>
<a-form-item-rest>
<a-input v-model:value="iconSelector.search" placeholder="搜索图标" allowClear />
</a-form-item-rest>
<a-tabs v-model:activeKey="iconSelector.activeTab" @change="iconTabHandleChange" tabPosition="left"
style="margin-top: 20px;">
<a-tab-pane v-for="(item, index) in iconDataSource" :key="index" :tab="item.type">
<div class="icon-wrapper">
<a-flex wrap="wrap" gap="small">
<a-button
v-for="item in iconData.slice((pagination.current - 1) * pagination.pageSize, pagination.current * pagination.pageSize)"
:icon="item.icon" @click="iconHandleClick(item)"
:class="updateState.icon === item.name ? 'active' : ''" />
</a-flex>
</div>
</a-tab-pane>
</a-tabs>
<div class="icon-pagination-wrapper">
<a-pagination size="small" v-model:current="pagination.current"
v-model:pageSize="pagination.pageSize" :total="pagination.total" :showTotal="pagination.showTotal"
:showQuickJumper="pagination.showQuickJumper" :showSizeChanger="false" />
</div>
</template>
<a-button :icon="updateState.icon ? h(icons[updateState.icon]) : ''" style="width: 250px;">{{
updateState.icon ? updateState.icon : '请选择图标' }}</a-button>
</a-popover>
</a-form-item>
<a-form-item name="order" label="排序">
<a-input-number v-model:value="updateState.order" :min="1" />
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
<div v-if="updateState.type !== 3">
<a-divider style="font-weight: 700">以下均为前端配置项</a-divider>
<a-form-item name="route_name" label="路由名称"
:rules="[{ required: updateState.type !== 3 ? true : false, message: '请输入路由名称' }]">
<a-input v-model:value="updateState.route_name" placeholder="请输入路由名称" allowClear></a-input>
</a-form-item>
<a-form-item name="route_path" label="路由路径"
:rules="[{ required: updateState.type !== 3 ? true : false, message: '请输入路由路径' }]">
<a-input v-model:value="updateState.route_path" placeholder="请输入路由路径" allowClear></a-input>
</a-form-item>
<a-form-item v-if="updateState.type === 1" name="redirect" label="重定向"
:rules="[{ required: updateState.type === 1 ? true : false, message: '请输入重定向' }]">
<a-input v-model:value="updateState.redirect" placeholder="请输入重定向" allowClear></a-input>
</a-form-item>
<a-form-item v-if="updateState.type === 2" name="component_path" label="组件地址"
:rules="[{ required: updateState.type === 2 ? true : false, message: '请输入组件地址' }]">
<a-input v-model:value="updateState.component_path" placeholder="请输入组件地址" allowClear></a-input>
</a-form-item>
<a-form-item name="cache" label="是否缓存"
:rules="[{ required: updateState.type !== 3 ? true : false, message: '请选择缓存状态' }]">
<a-switch v-model:checked="updateState.cache"></a-switch>
</a-form-item>
<a-form-item name="hidden" label="是否隐藏"
:rules="[{ required: updateState.type !== 3 ? true : false, message: '请选择隐藏状态' }]">
<a-switch v-model:checked="updateState.hidden"></a-switch>
</a-form-item>
</div>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, h, onMounted, watch } from 'vue';
import * as icons from '@ant-design/icons-vue';
import { message, Modal, Table } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined, ClearOutlined } from '@ant-design/icons-vue';
import { listToTree, cloneDeep, isEmpty } from '@/utils/util';
import axios from "axios"
import PageHeader from '@/components/PageHeader.vue';
import { getMenuList, createMenu, updateMenu, deleteMenu, batchAvailableMenu } from '@/api/system/menu'
import type { searchDataType, tableDataType } from './types'
// 响应式数据
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const menuSelectorTreeData = ref<tableDataType[]>([])
const dataSource = ref<tableDataType[]>([]);
const iconSelector = ref({ activeTab: 0, search: undefined });
const selectedRowKeys = ref<tableDataType['id'][]>([]);
let iconDataSource = [];
const iconData = ref([]);
const queryState = reactive<searchDataType>({
name: null,
available: null
});
const createState = reactive<tableDataType>({
name: '',
type: 1,
icon: '',
order: 1,
permission: '',
route_name: '',
route_path: '',
component_path: '',
redirect: '',
parent_id: undefined,
cache: true,
hidden: false,
available: true,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
type: 1,
icon: '',
order: 1,
permission: '',
route_name: '',
route_path: '',
component_path: '',
redirect: '',
parent_id: undefined,
cache: true,
hidden: false,
available: true,
description: ''
})
const detailState = ref<tableDataType>({});
const pagination = reactive({
current: 1,
pageSize: 70,
showQuickJumper: true,
showSizeChanger: false,
total: 0,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
})
const columns = reactive<TableColumnsType>([
{
title: '菜单名称',
dataIndex: 'name',
align: 'center',
},
{
title: '图标',
dataIndex: 'icon',
align: 'center',
},
{
title: '显示排序',
dataIndex: 'order',
align: 'center',
width: 80
},
{
title: '菜单类型',
dataIndex: 'type',
align: 'center',
width: 80
},
{
title: '权限标识',
dataIndex: 'permission',
align: 'center',
},
{
title: '状态',
dataIndex: 'available',
align: 'center',
},
{
title: '备注',
dataIndex: 'description',
align: 'center',
ellipsis: true,
width: 200
},
{
title: '操作',
dataIndex: 'operation',
fixed: 'right',
align: 'center',
width: 150
}
]);
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
checkStrictly: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 生命周期钩子
onMounted(() => {
loadingData();
axios.get('/icons.json').then(response => {
response.data.forEach((item, index) => {
iconDataSource.push({ type: item.type, icons: [] });
item.icons.forEach(icon => {
iconDataSource[index].icons.push({ name: icon, icon: h(icons[icon]) })
})
});
iconData.value = iconDataSource[iconSelector.value.activeTab].icons;
pagination.total = iconData.value.length;
}).catch(error => {
console.log(error)
})
});
// 查询
const onFinish = () => {
loadingData();
};
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.available) {
params['available'] = queryState.available == "true" ? true : false;
}
getMenuList(params).then(response => {
const result = response.data;
menuSelectorTreeData.value = listToTree(result.data.items.filter((item) => item.type !== 3));
dataSource.value = listToTree(result.data.items);
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
queryState.available = null
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, record?: tableDataType) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && record !== undefined) {
detailStateLoading.value = true;
detailState.value = record;
detailStateLoading.value = false;
} else if (modalType === 'update' && record !== undefined) {
Object.keys(updateState).forEach(key => {
updateState[key] = record[key];
})
}
iconSelector.value = {
activeTab: 0,
search: undefined
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteMenu({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 批量启用/停用
const handleMoreClick: MenuProps['onClick'] = e => {
if (!selectedRowKeys.value || !(selectedRowKeys.value.length > 0)) {
message.warning('请先勾选数据');
return;
}
Modal.confirm({
title: '提示',
content: e.key == 1 ? '是否确定启用选择项?' : '是否确定停用选择项?',
onOk() {
const body = { ids: selectedRowKeys.value, available: e.key == 1 ? true : false };
batchAvailableMenu(body).then(response => {
const result = response.data;
message.success(result.msg);
selectedRowKeys.value = [];
loadingData();
}).catch(error => {
console.log(error);
})
}
});
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createMenu(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key])
createState.type = 1;
createState.order = 1;
createState.cache = true;
createState.hidden = false;
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateMenu(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
// 监听菜单类型
watch(() => createState.type, (newType) => {
Object.keys(createState).forEach(key => delete createState[key])
createState.type = newType;
createState.order = 1;
createState.cache = true;
createState.hidden = false;
})
// 图标搜索
watch(() => iconSelector.value.search, (newSearchField) => iconSearch(newSearchField));
// 图标搜索
const iconSearch = (field) => {
let activeIcons = iconDataSource[iconSelector.value.activeTab].icons;
if (field) {
activeIcons = activeIcons.filter(item => item.name.toLowerCase().includes(field.toLowerCase()));
}
iconData.value = activeIcons;
pagination.current = 1;
pagination.total = iconData.value.length;
}
// 图标标签切换
const iconTabHandleChange = () => iconSearch(iconSelector.value.search);
// 图标点击
const iconHandleClick = (values) => {
if (modalTitle.value === 'create') {
createState.icon = values.name;
} else if (modalTitle.value === 'update') {
updateState.icon = values.name;
}
}
// 图标清空
const iconClearClickHandle = () => {
if (modalTitle.value === 'create') {
createState.icon = '';
} else if (modalTitle.value === 'update') {
updateState.icon = '';
}
}
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
.icon-wrapper {
min-width: 420px;
max-width: 420px;
min-height: 270px;
max-height: 270px;
}
.icon-clear-btn-wrapper {
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}
.icon-pagination-wrapper {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.active {
color: #4096ff;
border-color: #4096ff;
}
</style>
+33
View File
@@ -0,0 +1,33 @@
export interface searchDataType {
name?: string
available?: string
}
export interface tableDataType {
id?: number;
name?: string;
type?: number;
icon?: string;
order?: number;
permission?: string;
route_name?: string;
route_path?: string;
component_path?: string;
redirect?: string;
parent_id?: number;
parent_name?: string;
cache?: boolean;
hidden?: boolean;
available?: boolean;
description?: string;
created_at?: string;
updated_at?: string;
children?: tableDataType[];
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
+508
View File
@@ -0,0 +1,508 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.notice_title" placeholder="请输入标题" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="公告通知列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary"
:icon="h(PlusOutlined)"
@click="modalHandle('create')"
style="margin-right: 10px;">新建</a-button>
<a-button type="primary"
:icon="h(DownOutlined)"
@click="handleExport"
style="margin-right: 10px;">导出
</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="handleMoreClick">
<a-menu-item key="1"><span style="margin-right: 10px;">
<CheckOutlined />
</span><span>批量启用</span></a-menu-item>
<a-menu-item key="2"><span style="margin-right: 10px;">
<StopOutlined />
</span><span>批量停用</span></a-menu-item>
</a-menu>
</template>
<a-button>更多
<DownOutlined />
</a-button>
</a-dropdown>
</template>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }" :pagination="pagination"
:style="{ minHeight: '500px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'notice_title'">
<span :style="{ color: !record.available ? 'rgb(255, 77, 79)' : 'rgba(0, 0, 0, .88)' }">
{{ record.notice_title }}
</span>
</template>
<template v-if="column.dataIndex === 'notice_type'">
<span :style="{ color: record.notice_type === 1 ? '#108ee9' : '#f8e231' }">
{{ record.notice_type === 1 ? '通知' : '公告' }}
</span>
</template>
<template v-if="column.dataIndex === 'available'">
<span><a-badge :color="record.available ? 'green' : 'red'" /> {{ record.available ? '启用' :
'禁用' }}
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建公告通知' : (modalTitle === 'view' ? '查看公告通知' : '修改公告通知') }}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }"
:labelStyle="{ width: '140px' }" bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize +
detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="标题">{{ detailState.notice_title }}</a-descriptions-item>
<a-descriptions-item label="类型">
<a-tag :color="detailState.notice_type === 1 ? '#108ee9' : '#f8e231'">{{ detailState.notice_type === 1 ? '通知' : '公告' }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="内容">{{ detailState.notice_content }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :color="detailState.available ? 'green' : 'red'" />{{ detailState.available ?
'启用' :
'禁用' }}
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description
}}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="notice_title" label="标题" :rules="[{ required: true, message: '请输入标题' }]">
<a-input v-model:value="createState.notice_title" placeholder="请输入标题" allowClear></a-input>
</a-form-item>
<a-form-item name="notice_type" label="类型" :rules="[{ required: true, message: '请选择类型' }]">
<a-radio-group v-model:value="createState.notice_type">
<a-radio :value="1">通知</a-radio>
<a-radio :value="2">公告</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="notice_content" label="内容" :rules="[{ required: true, message: '请输入内容' }]">
<a-input v-model:value="createState.notice_content" placeholder="请输入内容" allowClear></a-input>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState"
v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="notice_title" label="标题" :rules="[{ required: true, message: '请输入标题' }]">
<a-input v-model:value="updateState.notice_title" placeholder="请输入标题" allowClear></a-input>
</a-form-item>
<a-form-item name="notice_type" label="类型" :rules="[{ required: true, message: '请选择类型' }]">
<a-radio-group v-model:value="updateState.notice_type">
<a-radio :value="1">通知</a-radio>
<a-radio :value="2">公告</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="notice_content" label="内容" :rules="[{ required: true, message: '请输入内容' }]">
<a-input v-model:value="updateState.notice_content" placeholder="请输入内容" allowClear></a-input>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4"
allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message, Modal } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getNoticeList, createNotice, updateNotice, deleteNotice, batchAvailableNotice, exportNotice} from '@/api/system/notice'
import type { searchDataType, tableDataType } from './types'
import XLSX from 'xlsx';
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({
notice_title: null,
available: null
});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
})
const createState = reactive<tableDataType>({
notice_title: '',
notice_type: 1,
notice_content: '',
available: true,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
notice_title: '',
notice_type: 1,
notice_content: '',
available: true,
description: ''
})
const detailState = ref<tableDataType>({})
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '标题',
dataIndex: 'notice_title',
align: 'center'
},
{
title: '类型',
dataIndex: 'notice_type',
align: 'center'
},
{
title: '内容',
dataIndex: 'notice_content',
align: 'center'
},
{
title: '状态',
dataIndex: 'available',
align: 'center'
},
{
title: '备注',
dataIndex: 'description',
align: 'center',
ellipsis: true,
// width: 500
},
{
title: '创建日期',
dataIndex: 'created_at',
align: 'center',
ellipsis: true,
// width: 120
},
{
title: '更新日期',
dataIndex: 'updated_at',
align: 'center',
ellipsis: true,
// width: 120
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.notice_title) {
params['notice_title'] = queryState.notice_title
}
if (queryState.available) {
params['available'] = queryState.available == true ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getNoticeList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.available = null
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteNotice({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 批量启用/停用
const handleMoreClick: MenuProps['onClick'] = e => {
if (!selectedRowKeys.value || !(selectedRowKeys.value.length > 0)) {
message.warning('请先勾选数据');
return;
}
Modal.confirm({
title: '提示',
content: e.key == 1 ? '是否确定启用选择项?' : '是否确定停用选择项?',
onOk() {
const body = { ids: selectedRowKeys.value, available: e.key == 1 ? true : false };
batchAvailableNotice(body).then(response => {
const result = response.data;
message.success(result.msg);
selectedRowKeys.value = [];
loadingData();
}).catch(error => {
console.log(error);
})
}
});
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createNotice(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateNotice(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
// 导出按钮操作
const handleExport = () => {
// 构建查询参数
const params = {
...queryState,
page_no: 1,
page_size: pagination.total // 导出所有数据
};
// 调用 exportLog 接口
exportNotice(params).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.ms-excel' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `notice_${new Date().getTime()}.xlsx`; // 设置下载文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
message.success('导出成功');
}).catch(error => {
console.error('导出失败:', error);
message.error('导出失败');
});
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+23
View File
@@ -0,0 +1,23 @@
export interface searchDataType {
notice_title?: string
available?: boolean
}
export interface tableDataType {
id?: number;
index?: number;
notice_title?: string;
notice_type?: number;
notice_content?: string;
available?: boolean;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
@@ -0,0 +1,486 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="岗位列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary"
:icon="h(PlusOutlined)"
@click="modalHandle('create')"
style="margin-right: 10px;">
新建
</a-button>
<a-button type="primary" :icon="h(DownOutlined)" @click="handleExport"
style="margin-right: 10px;">
导出
</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="handleMoreClick">
<a-menu-item key="1"><span style="margin-right: 10px;">
<CheckOutlined />
</span><span>批量启用</span></a-menu-item>
<a-menu-item key="2"><span style="margin-right: 10px;">
<StopOutlined />
</span><span>批量停用</span></a-menu-item>
</a-menu>
</template>
<a-button>更多
<DownOutlined />
</a-button>
</a-dropdown>
</template>
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:row-selection="rowSelection"
:loading="tableLoading"
@change="handleTableChange"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:pagination="pagination"
:style="{ minHeight: '500px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'name'">
<span :style="{ color: !record.available ? 'rgb(255, 77, 79)' : 'rgba(0, 0, 0, .88)' }">
{{ record.name }}
</span>
</template>
<template v-if="column.dataIndex === 'available'">
<span><a-badge :color="record.available ? 'green' : 'red'" /> {{ record.available ? '启用' : '禁用' }}
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal
v-model:open="openModal"
@ok="handleModalSumbit"
:width="800"
:destroyOnClose="true"
:confirmLoading="modalSubmitLoading"
style="top: 30px"
>
<template #title>
<span>{{ modalTitle === 'create' ? '新建岗位' : (modalTitle === 'view' ? '查看岗位' : '修改岗位') }}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }" :labelStyle="{ width: '140px' }"
bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize + detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="排序">{{ detailState.order }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :color="detailState.available ? 'green' : 'red'" />{{ detailState.available ? '启用' : '禁用' }}
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="名称" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="createState.order" :min="1" />
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="名称" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="updateState.order" :min="1" />
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message, Modal } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getPositionList, createPosition, updatePosition, deletePosition, batchAvailablePosition, exportPosition } from '@/api/system/position'
import type { searchDataType, tableDataType } from './types'
import XLSX from 'xlsx';
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({
name: null,
available: null
});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
})
const createState = reactive<tableDataType>({
name: '',
order: 1,
available: true,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
order: 1,
available: true,
description: ''
})
const detailState = ref<tableDataType>({})
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '名称',
dataIndex: 'name',
align: 'center'
},
{
title: '排序',
dataIndex: 'order',
align: 'center'
},
{
title: '状态',
dataIndex: 'available',
align: 'center'
},
{
title: '备注',
dataIndex: 'description',
align: 'center',
ellipsis: true,
// width: 500
},
{
title: '创建日期',
dataIndex: 'created_at',
align: 'center',
ellipsis: true,
// width: 120
},
{
title: '更新日期',
dataIndex: 'updated_at',
align: 'center',
ellipsis: true,
// width: 120
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
width: 150
}
];
// 表格选中配置
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.available) {
params['available'] = queryState.available == "true" ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getPositionList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.available = null
loadingData();
}
// 表格分页处理
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
updateState[key] = selected[key];
})
}
}
// 删除
const deleteRow = (row: tableDataType) => {
deletePosition({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 批量启用/停用
const handleMoreClick: MenuProps['onClick'] = e => {
if (!selectedRowKeys.value || !(selectedRowKeys.value.length > 0)) {
message.warning('请先勾选数据');
return;
}
Modal.confirm({
title: '提示',
content: e.key == 1 ? '是否确定启用选择项?' : '是否确定停用选择项?',
onOk() {
const body = { ids: selectedRowKeys.value, available: e.key == 1 ? true : false };
batchAvailablePosition(body).then(response => {
const result = response.data;
message.success(result.msg);
selectedRowKeys.value = [];
loadingData();
}).catch(error => {
console.log(error);
})
}
});
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createPosition(createBody).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
createState.order = 1;
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updatePosition(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
message.success(response.data.msg);
loadingData();
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
/** 导出按钮操作 */
const handleExport = () => {
// 构建查询参数
const params = {
...queryState,
page_no: 1,
page_size: pagination.total // 导出所有数据
};
// 调用 exportLog 接口
exportPosition(params).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.ms-excel' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `position_${new Date().getTime()}.xlsx`; // 设置下载文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
message.success('导出成功');
}).catch(error => {
console.error('导出失败:', error);
message.error('导出失败');
});
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
@@ -0,0 +1,22 @@
export interface searchDataType {
name?: string
available?: string
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
order?: number;
available?: boolean;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
@@ -0,0 +1,223 @@
<template>
<!-- 授权弹窗 -->
<a-drawer v-model:open="openDrawer" :destroyOnClose="true" :title="`授权 - ${currentRole?.name || ''}`" placement="right" :width="1500"
:bodyStyle="{ padding: 'none' }">
<template #extra>
<a-button type="primary" @click="handleDrawerSave" :loading="drawerSaving">保存</a-button>
</template>
<div style="display: flex;">
<div style="min-width: 300px;">
<div style="display: flex; gap: 10px; ">
<div style="width: 10px; background-color: #1677ff;"></div>
<div>
<span style="font-size: 16px;">数据授权</span>
<a-tooltip placement="right">
<template #title>
<span>授权用户可操作的数据范围</span>
</template>
<QuestionCircleOutlined style="margin-left: 5px;" />
</a-tooltip>
</div>
</div>
<a-select v-model:value="permissionState.data_scope" style="width: 80%; margin-top: 15px;">
<a-select-option :value="1">仅本人数据权限</a-select-option>
<a-select-option :value="2">本部门数据权限</a-select-option>
<a-select-option :value="3">本部门及以下数据权限</a-select-option>
<a-select-option :value="4">全部数据权限</a-select-option>
<a-select-option :value="5">自定义数据权限</a-select-option>
</a-select>
<a-tree v-if="permissionState.data_scope === 5 && deptTreeData.length"
:checkedKeys="permissionState.dept_ids"
:rowKey="record => record.id"
:tree-data="deptTreeData"
:defaultExpandAll="true"
:field-names="{ children: 'children', title: 'name', key: 'id' }"
@check="deptTreeCheck" checkable checkStrictly style="margin-top: 15px;" />
</div>
<a-divider type="vertical" style="height: 80vh;" />
<div>
<div style="display: flex; gap: 10px;">
<div style="width: 10px; background-color: #1677ff;"></div>
<div>
<span style="font-size: 16px;">菜单授权</span>
<a-tooltip placement="right">
<template #title>
<span>授权用户在菜单中可操作的范围</span>
</template>
<QuestionCircleOutlined style="margin-left: 5px;" />
</a-tooltip>
</div>
</div>
<div style="margin-top: 15px;">
<a-table :rowKey="record => record.id"
:columns="menuColumns"
:data-source="menuTreeData"
:row-selection="menuRowSelection"
:loading="tableLoading"
:scroll="{ x: 500, y: 'calc(100vh - 270px)' }"
:pagination="false"
:style="{ minHeight: '700px' }"
:expandAll="true">
<template v-slot:bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'type'">
<a-tag :color="record.type === 1 ? 'blue' : (record.type === 2 ? 'green' : 'orange')">
{{ record.type === 1 ? '目录' : (record.type === 2 ? '功能' : '权限') }}
</a-tag>
</template>
<template v-if="column.dataIndex === 'available'">
<span><a-badge :color="record.available ? 'green' : 'red'" /> {{ record.available ? '启用'
: '禁用' }}
</span>
</template>
</template>
</a-table>
</div>
</div>
</div>
<template #footer>
<div style="height: 50px;"></div>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { listToTree } from '@/utils/util';
import { message } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import type { tableDataType, permissionDataType, permissionDeptType, permissionMenuType } from './types'
import { getMenuList } from '@/api/system/menu';
import { getDeptList } from '@/api/system/dept';
import { setPermission } from '@/api/system/role';
const openDrawer = ref(false);
const currentRole = ref(null);
const permissionState = ref<permissionDataType>({
role_ids: [],
menu_ids: [],
data_scope: 1,
dept_ids: []
});
const deptTreeData = ref<permissionDeptType[]>([]);
const menuTreeData = ref<permissionMenuType[]>([]);
const tableLoading = ref(false);
const drawerSaving = ref(false);
const menuColumns: TableColumnsType = [
{
title: '菜单名称',
dataIndex: 'name'
},
{
title: '菜单类型',
dataIndex: 'type',
width: 100
},
{
title: '权限标识',
dataIndex: 'permission',
},
{
title: '状态',
dataIndex: 'available',
width: 100
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
width: 400
}
];
// 初始化方法,用于打开抽屉并加载数据
const init = async (record: tableDataType) => {
if (!record) {
message.error('请选择角色')
return
}
openDrawer.value = true;
tableLoading.value = true;
try {
// 获取部门树
const deptResponse = await getDeptList();
deptTreeData.value = listToTree(deptResponse.data.data.items);
// 获取菜单树
const menuResponse = await getMenuList();
menuTreeData.value = listToTree(menuResponse.data.data.items);
currentRole.value = record;
// 初始化权限状态
permissionState.value = {
role_ids: [record.id],
menu_ids: record.menus?.map(menu => menu.id) || [],
data_scope: record.data_scope || 1,
dept_ids: record.depts?.map(dept => dept.id) || []
}
} catch (error) {
console.error('获取权限数据失败:', error);
message.error('获取权限数据失败');
} finally {
tableLoading.value = false;
}
}
// 部门树选择回调
const deptTreeCheck = (checkedKeys) => {
permissionState.value.dept_ids = checkedKeys.checked;
}
const emit = defineEmits(['event']);
// 保存权限设置
const handleDrawerSave = () => {
drawerSaving.value = true;
setPermission(permissionState.value).then(response => {
message.success(response.data.msg);
drawerSaving.value = false;
openDrawer.value = false;
emit('event');
}).catch(error => {
console.error('保存权限失败:', error);
message.error('保存权限失败');
drawerSaving.value = false;
})
}
// 菜单选择变更回调
const onMenuSelectChange = (selectingRowKeys: permissionMenuType['id'][]) => {
permissionState.value.menu_ids = selectingRowKeys;
}
// 菜单表格选择配置
const menuRowSelection = computed(() => {
return {
selectedRowKeys: permissionState.value.menu_ids,
checkStrictly: true,
onChange: onMenuSelectChange
}
});
defineExpose({
init,
permissionState
});
</script>
<style lang="scss" scoped></style>
+498
View File
@@ -0,0 +1,498 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 表格搜索 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="角色列表"
:bordered="false"
:headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')"
style="margin-right: 10px;">
新建
</a-button>
<a-button type="primary" :icon="h(DownOutlined)" @click="handleExport"
style="margin-right: 10px;">
导出
</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="handleMoreClick">
<a-menu-item key="1"><span style="margin-right: 10px;">
<CheckOutlined />
</span><span>批量启用</span></a-menu-item>
<a-menu-item key="2"><span style="margin-right: 10px;">
<StopOutlined />
</span><span>批量停用</span></a-menu-item>
</a-menu>
</template>
<a-button>更多
<DownOutlined />
</a-button>
</a-dropdown>
</template>
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:row-selection="rowSelection"
:loading="tableLoading"
@change="handleTableChange"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:pagination="pagination"
:style="{ minHeight: '500px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'name'">
<span :style="{ color: !record.available ? 'rgb(255, 77, 79)' : 'rgba(0, 0, 0, .88)' }">
{{ record.name }}
</span>
</template>
<template v-if="column.dataIndex === 'available'">
<span><a-badge :color="record.available ? 'green' : 'red'" /> {{ record.available ? '启用' : '禁用' }}
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a @click="modalHandle('view', index)">查看</a>
<a @click="modalHandle('update', index)">修改</a>
<a @click="modalHandle('permission', index)">授权</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建角色' : (modalTitle === 'view' ? '查看角色' : '修改角色') }}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }" :labelStyle="{ width: '140px' }"
bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize + detailState.index + 1 }}</a-descriptions-item>
<a-descriptions-item label="名称">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="排序">{{ detailState.order }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :color="detailState.available ? 'green' : 'red'" />{{ detailState.available ? '启用' : '禁用' }}
</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-' }}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="名称" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="createState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="createState.order" :min="1" />
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="name" label="名称" :rules="[{ required: true, message: '请输入名称' }]">
<a-input v-model:value="updateState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
<a-form-item name="order" label="排序" :rules="[{ required: true, message: '请输入排序' }]">
<a-input-number v-model:value="updateState.order" :min="1" />
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
<!-- 授权弹窗 -->
<PermissionDrawer ref="permissionDrawer" @event="handlePermissionDrawerEvent" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message, Modal } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { cloneDeep, isEmpty } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import PermissionDrawer from './PermissionDrawer.vue'
import type { searchDataType, tableDataType } from './types'
import { getRoleList, createRole, updateRole, deleteRole, batchAvailableRole, exportRole } from '@/api/system/role'
import XLSX from 'xlsx';
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const permissionDrawer = ref();
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState: searchDataType = reactive({
name: null,
available: null
});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
})
const createState = reactive<tableDataType>({
name: '',
order: 1,
available: true,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
order: 1,
available: true,
description: ''
})
const detailState = ref<tableDataType>({})
const columns = reactive<TableColumnsType>([
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '名称',
dataIndex: 'name',
align: 'center',
// width: 80
},
{
title: '排序',
dataIndex: 'order',
align: 'center',
width: 80
},
{
title: '状态',
dataIndex: 'available',
align: 'center',
// width: 80
},
{
title: '备注',
dataIndex: 'description',
align: 'center',
ellipsis: true,
width: 150
},
{
title: '创建日期',
dataIndex: 'created_at',
align: 'center',
ellipsis: true,
// width: 120
},
{
title: '更新日期',
dataIndex: 'updated_at',
align: 'center',
ellipsis: true,
// width: 120
},
{
title: '操作',
dataIndex: 'operation',
align: 'center',
fixed: 'right',
width: 180
}
]);
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.available) {
params['available'] = queryState.available == "true" ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getRoleList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.error(error);
}).finally(() => {
tableLoading.value = false;
})
}
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.available = null
loadingData();
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteRole({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error)
})
}
// 批量启用/停用
const handleMoreClick: MenuProps['onClick'] = e => {
if (!selectedRowKeys.value || !(selectedRowKeys.value.length > 0)) {
message.warning('请先勾选数据');
return;
}
Modal.confirm({
title: '提示',
content: e.key == 1 ? '是否确定启用选择项?' : '是否确定停用选择项?',
onOk() {
const body = { ids: selectedRowKeys.value, available: e.key == 1 ? true : false };
batchAvailableRole(body).then(response => {
const result = response.data;
message.success(result.msg);
selectedRowKeys.value = [];
loadingData();
}).catch(error => {
console.log(error);
})
}
});
}
// 表格分页
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
Object.keys(updateState).forEach(key => {
updateState[key] = dataSource.value[index][key];
})
} else if (modalType === 'permission' && index !== undefined) {
openModal.value = false;
permissionDrawer.value.init(dataSource.value[index]);
}
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createRole(createBody).then(response => {
const result = response.data;
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
createState.order = 1;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateRole(updateState).then(response => {
const result = response.data;
modalSubmitLoading.value = false;
openModal.value = false;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.log(error)
})
}
}
/** 导出按钮操作 */
const handleExport = () => {
// 构建查询参数
const params = {
...queryState,
page_no: 1,
page_size: pagination.total // 导出所有数据
};
// 调用 exportLog 接口
exportRole(params).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.ms-excel' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `role_${new Date().getTime()}.xlsx`; // 设置下载文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
message.success('导出成功');
}).catch(error => {
console.error('导出失败:', error);
message.error('导出失败');
});
};
// 授权弹窗事件
const handlePermissionDrawerEvent = () => loadingData();
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+51
View File
@@ -0,0 +1,51 @@
export interface searchDataType {
name?: string
available?: string
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
order?: number;
data_scope?: number;
available?: boolean;
description?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
menus?: permissionMenuType[];
depts?: permissionDeptType[];
}
export interface permissionDataType {
role_ids?: tableDataType['id'][];
menu_ids?: permissionMenuType['id'][];
data_scope?: number;
dept_ids?: number[];
}
export interface permissionDeptType {
id?: number;
name?: string;
parent_id?: number;
children?: permissionDeptType[];
}
export interface permissionMenuType {
id?: number;
name?: string;
type?: number;
permission?: string;
parent_id?: number;
available?: boolean;
description?: string;
children?: permissionMenuType[];
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}
@@ -0,0 +1,225 @@
<template>
<a-modal :title="title" v-model:open="openModal" :width="1200" :destroyOnClose="true" style="top: 30px">
<template #footer>
<a-button @click="handleModalCancel">取消</a-button>
<a-button @click="handleModalClear">清空</a-button>
<a-button type="primary" @click="handleModalSumbit">确定</a-button>
</template>
<div class="table-search-wrapper">
<a-card :bordered="true">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState.available" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<div>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource" :row-selection="rowSelection"
:loading="tableLoading" @change="handleTableChange" :scroll="{ x: 500, y: 330 }" :pagination="pagination"
:style="{ minHeight: '330px' }">
<template v-slot:bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'available'">
<span><a-badge :color="record.available ? 'green' : 'red'" /> {{ record.available ? '启用' : '禁用' }}
</span>
</template>
</template>
</a-table>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref } from 'vue';
import { Table } from 'ant-design-vue';
import type { TableColumnsType } from 'ant-design-vue';
import { getRoleList } from '@/api/system/role'
import { getPositionList } from '@/api/system/position'
import type { searchSelectDataType, roleSelectorType, positionSelectorType } from './types.ts';
const subject = ref('');
const openModal = ref(false);
const tableLoading = ref(false);
const dataSource = ref<roleSelectorType[] | positionSelectorType[]>([]);
const selectedRowKeys = ref<roleSelectorType['id'][] | positionSelectorType['id'][]>([]);
const selectedRowItemNames = ref<roleSelectorType['name'][] | positionSelectorType['name'][]>([]);
const queryState: searchSelectDataType = reactive({
name: "",
available: 'true'
});
const pagination = reactive({
current: 1,
pageSize: 20,
defaultPageSize: 20,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
})
const columns: TableColumnsType = [
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '名称',
dataIndex: 'name',
align: 'center'
},
{
title: '状态',
dataIndex: 'available',
align: 'center'
},
{
title: '备注',
dataIndex: 'description',
align: 'center',
ellipsis: true,
width: 500
}
];
const title = computed(() => {
return subject.value === 'role' ? '选择角色' : '选择岗位';
});
const loadingData = () => {
if (!subject.value) {
return;
}
tableLoading.value = true;
dataSource.value = [];
let params = {};
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.available) {
params['available'] = queryState.available == "true" ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
const requestApi = subject.value == 'role' ? getRoleList(params) : getPositionList(params);
requestApi.then(response => {
const result = response.data;
dataSource.value = result.data.items;
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
tableLoading.value = false;
}).catch(error => {
console.log(error);
tableLoading.value = false;
})
}
const onFinish = () => {
pagination.current = 1;
loadingData();
};
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.available = "true"
loadingData();
}
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
const onSelectChange = (
selectingRowKeys: roleSelectorType['id'][] | positionSelectorType['id'][],
selectingRows: roleSelectorType[] | positionSelectorType[],
) => {
selectedRowKeys.value = selectingRowKeys;
selectedRowItemNames.value = selectingRows.map(row => row.name);
}
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: onSelectChange,
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
const emit = defineEmits(['event']);
const handleModalSumbit = () => {
emit('event', subject.value, selectedRowKeys.value, selectedRowItemNames.value);
handleModalCancel();
}
const handleModalClear = () => {
handleModalCancel();
emit('event', subject.value, selectedRowKeys.value, selectedRowItemNames.value);
}
const handleModalCancel = () => {
openModal.value = false;
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
queryState.available = "true"
pagination.current = 1;
pagination.pageSize = pagination.defaultPageSize;
selectedRowKeys.value = [];
selectedRowItemNames.value = [];
}
defineExpose({
subject,
openModal,
selectedRowKeys,
selectedRowItemNames,
loadingData
});
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+846
View File
@@ -0,0 +1,846 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<!-- 表格搜索区域 -->
<div class="table-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="username" label="用户名" style="max-width: 300px;">
<a-input v-model:value="queryState['username']" placeholder="请输入用户名" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="name" label="姓名" style="max-width: 300px;">
<a-input v-model:value="queryState['name']" placeholder="请输入姓名" allowClear></a-input>
</a-form-item>
</a-col>
<a-col flex="0 1 450px">
<a-form-item name="available" label="状态" style="max-width: 300px;">
<a-select v-model:value="queryState['available']" placeholder="全部" allowClear>
<a-select-option value="true">启用</a-select-option>
<a-select-option value="false">停用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button style="margin: 0 8px" @click="resetFields">重置</a-button>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="用户列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')" style="margin-right: 10px;">
新建
</a-button>
<a-button type="primary" :icon="h(UploadOutlined)" @click="modalHandle('import')" style="margin-right: 10px;">
导入
</a-button>
<a-button type="primary" :icon="h(DownOutlined)" @click="handleExport" style="margin-right: 10px;">
导出
</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="handleMoreClick">
<a-menu-item key="1"><span style="margin-right: 10px;">
<CheckOutlined />
</span><span>批量启用</span></a-menu-item>
<a-menu-item key="2"><span style="margin-right: 10px;">
<StopOutlined />
</span><span>批量停用</span></a-menu-item>
</a-menu>
</template>
<a-button>更多
<DownOutlined />
</a-button>
</a-dropdown>
</template>
<a-table :rowKey="record => record.id" :columns="columns" :data-source="dataSource"
:row-selection="rowSelection" :loading="tableLoading" @change="handleTableChange"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }" :pagination="pagination" :style="{ minHeight: '500px' }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.dataIndex === 'index'">
<span>{{ (pagination.current - 1) * pagination.pageSize + index + 1 }}</span>
</template>
<template v-if="column.dataIndex === 'name'">
<span :style="{ color: !record.available ? 'rgb(255, 77, 79)' : 'rgba(0, 0, 0, .88)' }">
{{ record.name }}
</span>
</template>
<template v-if="column.dataIndex === 'dept'">
<span>{{ record.dept_name }}</span>
</template>
<template v-if="column.dataIndex === 'roles'">
<span>{{ record.roleNames }}</span>
</template>
<template v-if="column.dataIndex === 'positions'">
<span>{{ record.positionNames }}</span>
</template>
<template v-if="column.dataIndex === 'gender'">
<a-tag :color="record.gender === 1 ? 'blue' : 'pink'">{{ record.gender === 1 ? '男' : '女' }}</a-tag>
</template>
<template v-if="column.dataIndex === 'available'">
<span>
<a-badge :color="record.available ? 'green' : 'red'" />
{{ record.available ? '启用' : '禁用' }}
</span>
</template>
<template v-if="column.dataIndex === 'is_superuser'">
<span>
<a-badge :color="record.is_superuser ? 'green' : 'red'" />
{{ record.is_superuser ? '是' : '否' }}
</span>
</template>
<template v-if="column.dataIndex === 'operation'">
<a-space size="middle">
<a v-on:click="modalHandle('view', index)">查看</a>
<a v-on:click="modalHandle('update', index)">修改</a>
<a-popconfirm title="确定删除吗?" ok-text="确定" cancel-text="取消" @confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 弹窗区域 -->
<div class="modal-wrapper">
<a-modal v-model:open="openModal" @ok="handleModalSumbit" :width="800" :destroyOnClose="true"
:confirmLoading="modalSubmitLoading" style="top: 30px">
<template #title>
<span>{{ modalTitle === 'create' ? '新建用户' : (modalTitle === 'view' ? '查看用户' : (modalTitle === 'import' ? '导入用户' : '修改用户')) }}</span>
</template>
<div v-if="modalTitle === 'view'">
<a-spin :spinning="detailStateLoading">
<a-descriptions :column="{ xxl: 2, xl: 2, lg: 2, md: 2, sm: 1, xs: 1 }" :labelStyle="{ width: '140px' }"
bordered>
<a-descriptions-item label="序号">{{ (pagination.current - 1) * pagination.pageSize + detailState.index + 1
}}</a-descriptions-item>
<a-descriptions-item label="用户名">{{ detailState.username }}</a-descriptions-item>
<a-descriptions-item label="姓名">{{ detailState.name }}</a-descriptions-item>
<a-descriptions-item label="性别">{{ detailState.gender === 1 ? '男' : '女' }}</a-descriptions-item>
<a-descriptions-item label="部门" :span="2">{{ detailState.dept_name }}</a-descriptions-item>
<a-descriptions-item label="角色" :span="2">{{ detailState.roleNames }}</a-descriptions-item>
<a-descriptions-item label="岗位" :span="2">{{ detailState.positionNames }}</a-descriptions-item>
<a-descriptions-item label="邮箱">{{ detailState.email }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ detailState.mobile }}</a-descriptions-item>
<a-descriptions-item label="是否超管">
<a-badge :color="detailState.is_superuser ? 'green' : 'red'" />{{ detailState.is_superuser ? '是' : '否'
}}
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-badge :color="detailState.available ? 'green' : 'red'" />{{ detailState.available ? '启用' : '禁用' }}
</a-descriptions-item>
<a-descriptions-item label="上次登录时间">{{ detailState.last_login }}</a-descriptions-item>
<a-descriptions-item label="创建人">{{ detailState.creator ? detailState.creator.name : '-'
}}</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ detailState.created_at }}</a-descriptions-item>
<a-descriptions-item label="修改时间">{{ detailState.updated_at }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ detailState.description }}</a-descriptions-item>
</a-descriptions>
</a-spin>
</div>
<div v-else-if="modalTitle === 'import'">
<!-- 用户导入对话框 -->
<a-modal :title="upload.title" v-model:visible="upload.open" width="400px" centered>
<a-upload ref="uploadRef"
:before-upload="beforeUpload"
:multiple="false"
:accept="'.xlsx, .xls'"
:headers="upload.headers"
:action="upload.url + '?updateSupport=' + upload.updateSupport"
:disabled="upload.isUploading"
:custom-request="customRequest"
:show-upload-list="false" drag>
<p class="ant-upload-drag-icon">
<upload-outlined />
</p>
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
<p class="ant-upload-hint">
<a-checkbox v-model:checked="upload.updateSupport" /> 是否更新已存在的用户数据
</p>
<p class="ant-upload-hint">
仅允许导入 xls、xlsx 格式的文件。
<a :href="importUserTemplate" target="_blank"
style="font-size: 12px; vertical-align: baseline;">下载模板</a>
</p>
</a-upload>
<template #footer>
<a-button type="primary" @click="submitFileForm">确 定</a-button>
<a-button @click="upload.open = false">取 消</a-button>
</template>
</a-modal>
</div>
<div v-else-if="modalTitle === 'create'">
<a-form ref="createForm" :model="createState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="username" label="用户名" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model:value="createState.username" placeholder="请输入用户名" allowClear></a-input>
</a-form-item>
<a-form-item name="name" label="姓名" :rules="[{ required: true, message: '请输入姓名' }]">
<a-input v-model:value="createState.name" placeholder="请输入姓名" allowClear></a-input>
</a-form-item>
<a-form-item name="dept_id" label="部门" :rules="[{ required: true, message: '请选择部门' }]">
<a-tree-select v-model:value="createState.dept_id"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" :tree-data="deptTreeData"
:field-names="{ children: 'children', label: 'name', value: 'id' }" placeholder="请选择部门"
tree-node-filter-prop="name" style="width: 100%" show-search allow-clear></a-tree-select>
</a-form-item>
<a-form-item name="role_ids" label="角色">
<a-select v-model:value="createState.roleNames" :open="false" @click="selectModalHandle('role')"
placeholder="请选择角色">
<template #suffixIcon>
<SearchOutlined />
</template>
</a-select>
</a-form-item>
<a-form-item name="position_ids" label="岗位">
<a-select v-model:value="createState.positionNames" :open="false" @click="selectModalHandle('position')"
placeholder="请选择岗位">
<template #suffixIcon>
<SearchOutlined />
</template>
</a-select>
</a-form-item>
<a-form-item name="password" label="密码" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model:value="createState.password" placeholder="请输入密码" allowClear></a-input-password>
</a-form-item>
<a-form-item name="gender" label="性别" :rules="[{ required: true, message: '请选择性别' }]">
<a-select v-model:value="createState.gender" placeholder="请选择性别" allowClear>
<a-select-option :value="1">男</a-select-option>
<a-select-option :value="2">女</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="email" label="邮箱">
<a-input v-model:value="createState.email" placeholder="请输入邮箱" allowClear></a-input>
</a-form-item>
<a-form-item name="mobile" label="联系电话">
<a-input v-model:value="createState.mobile" placeholder="请输入电话" allowClear></a-input>
</a-form-item>
<a-form-item name="is_superuser" label="是否超管" :rules="[{ required: true, message: '请选择是否超管' }]">
<a-radio-group v-model:value="createState.is_superuser">
<a-radio :value="true">是</a-radio>
<a-radio :value="false">否</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="createState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="createState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
<div v-else>
<a-form ref="updateForm" :model="updateState" v-bind="{ labelCol: { span: 5 }, wrapperCol: { span: 15 } }">
<a-form-item name="username" label="用户名" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model:value="updateState.username" placeholder="请输入用户名" allowClear></a-input>
</a-form-item>
<a-form-item name="name" label="姓名" :rules="[{ required: true, message: '请输入姓名' }]">
<a-input v-model:value="updateState.name" placeholder="请输入姓名" allowClear></a-input>
</a-form-item>
<a-form-item name="dept_id" label="部门" :rules="[{ required: true, message: '请选择部门' }]">
<a-tree-select v-model:value="updateState.dept_id"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }" :tree-data="deptTreeData"
:field-names="{ children: 'children', label: 'name', value: 'id' }" placeholder="请选择部门"
tree-node-filter-prop="name" style="width: 100%" show-search allow-clear></a-tree-select>
</a-form-item>
<a-form-item name="role_ids" label="角色">
<a-select v-model:value="updateState.roleNames" :open="false" @click="selectModalHandle('role')"
placeholder="请选择角色">
<template #suffixIcon>
<SearchOutlined />
</template>
</a-select>
</a-form-item>
<a-form-item name="position_ids" label="岗位">
<a-select v-model:value="updateState.positionNames" :open="false" @click="selectModalHandle('position')"
placeholder="请选择岗位">
<template #suffixIcon>
<SearchOutlined />
</template>
</a-select>
</a-form-item>
<a-form-item v-if="!showPasswordInput" label="修改密码">
<a-checkbox v-model:checked="showPasswordInput"></a-checkbox>
</a-form-item>
<a-form-item v-else name="password" label="密码">
<a-input-password v-model:value="updateState.password" placeholder="请输入密码" allowClear></a-input-password>
</a-form-item>
<a-form-item name="gender" label="性别" :rules="[{ required: true, message: '请选择性别' }]">
<a-select v-model:value="updateState.gender" placeholder="请选择性别" allowClear>
<a-select-option :value="1">男</a-select-option>
<a-select-option :value="2">女</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="email" label="邮箱">
<a-input v-model:value="updateState.email" placeholder="请输入邮箱" allowClear></a-input>
</a-form-item>
<a-form-item name="mobile" label="联系电话">
<a-input v-model:value="updateState.mobile" placeholder="请输入电话" allowClear></a-input>
</a-form-item>
<a-form-item name="is_superuser" label="是否超管" :rules="[{ required: true, message: '请选择是否超管' }]">
<a-radio-group v-model:value="updateState.is_superuser">
<a-radio :value="true">是</a-radio>
<a-radio :value="false">否</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="available" label="状态" :rules="[{ required: true, message: '请选择状态' }]">
<a-radio-group v-model:value="updateState.available">
<a-radio :value="true">启用</a-radio>
<a-radio :value="false">停用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item name="description" label="备注">
<a-textarea v-model:value="updateState.description" placeholder="请输入备注" :rows="4" allowClear />
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
<!-- 选择器弹窗 -->
<SelectorModal ref="selectorModal" @event="handleSelectorModalEvent" />
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, unref, onMounted, h } from 'vue';
import { Table, message, Modal } from 'ant-design-vue';
import type { TableColumnsType, MenuProps } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, UploadOutlined, CheckOutlined, StopOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { isEmpty, listToTree } from '@/utils/util';
import PageHeader from '@/components/PageHeader.vue';
import { getDeptList } from '@/api/system/dept'
import { getUserList, createUser, updateUser, deleteUser, batchAvailableUser, exportUser, importUserTemplate, importUserData } from '@/api/system/user'
import SelectorModal from './SelectorModal.vue'
import type { searchDataType, tableDataType, deptTreeType, roleSelectorType, positionSelectorType } from './types'
import XLSX from 'xlsx';
import store from '@/store';
import storage from 'store';
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const showPasswordInput = ref(false);
const createForm = ref();
const updateForm = ref();
const selectorModal = ref();
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const deptTreeData = ref<deptTreeType[]>([]);
const queryState = reactive<searchDataType>({
username: null,
name: null,
available: null
});
const pagination = reactive({
current: 1,
pageSize: 10,
defaultPageSize: 10,
showSizeChanger: true,
total: dataSource.value.length,
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条 / 总共 ${total} 条`
})
const createState = reactive<tableDataType>({
username: '',
name: '',
dept_id: undefined,
dept_name: '',
role_ids: undefined,
roleNames: undefined,
position_ids: undefined,
positionNames: undefined,
password: '',
gender: undefined,
email: '',
mobile: '',
is_superuser: false,
available: true,
description: ''
})
const updateState = reactive<tableDataType>({
id: undefined,
username: '',
name: '',
dept_id: undefined,
dept_name: '',
role_ids: [],
roleNames: undefined,
position_ids: [],
positionNames: undefined,
password: '',
gender: undefined,
email: '',
mobile: '',
is_superuser: false,
available: true,
description: '',
})
const detailState = ref<tableDataType>({});
const columns = reactive<TableColumnsType>([
{
title: '序号',
dataIndex: 'index',
align: 'center',
width: 80
},
{
title: '用户名',
dataIndex: 'username',
ellipsis: true,
align: 'center',
width: 80
},
{
title: '姓名',
dataIndex: 'name',
ellipsis: true,
align: 'center',
width: 80
},
{
title: '是否超管',
dataIndex: 'is_superuser',
align: 'center',
width: 80
},
{
title: '部门',
dataIndex: 'dept',
ellipsis: true,
align: 'center',
width: 80
},
{
title: '角色',
dataIndex: 'roles',
ellipsis: true,
align: 'center',
width: 100
},
{
title: '岗位',
dataIndex: 'positions',
ellipsis: true,
align: 'center',
width: 100
},
{
title: '邮箱',
dataIndex: 'email',
align: 'center',
// width: 160
},
{
title: '联系电话',
dataIndex: 'mobile',
align: 'center',
width: 120
},
{
title: '性别',
dataIndex: 'gender',
align: 'center',
width: 80
},
{
title: '状态',
dataIndex: 'available',
align: 'center',
width: 80
},
{
title: '备注',
dataIndex: 'description',
ellipsis: true,
align: 'center',
// width: 200
},
{
title: '操作',
dataIndex: 'operation',
fixed: 'right',
align: 'center',
width: 150
}
]);
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
pagination.current = 1;
loadingData();
};
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.username) {
params['username'] = queryState.username
}
if (queryState.name) {
params['name'] = queryState.name
}
if (queryState.available) {
params['available'] = queryState.available == "true" ? true : false;
}
params['page_no'] = pagination.current
params['page_size'] = pagination.pageSize
getDeptList().then(response => {
const result = response.data;
deptTreeData.value = listToTree(result.data.items);
})
getUserList(params).then(response => {
const result = response.data;
dataSource.value = result.data.items.map((item: tableDataType) => {
item.roleNames = item.roles ? item.roles.map(item => item.name).join(",") : undefined;
item.positionNames = item.positions ? item.positions.map(item => item.name).join(",") : undefined;
return item;
});
pagination.total = result.data.total;
pagination.current = result.data.page_no;
pagination.pageSize = result.data.page_size;
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
}
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
pagination.current = 1;
queryState.available = null
loadingData();
}
// 删除
const deleteRow = (row: tableDataType) => {
deleteUser({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error);
})
}
// 批量启用/停用
const handleMoreClick: MenuProps['onClick'] = e => {
if (!selectedRowKeys.value || !(selectedRowKeys.value.length > 0)) {
message.warning('请先勾选数据');
return;
}
Modal.confirm({
title: '提示',
content: e.key == 1 ? '是否确定启用选择项?' : '是否确定停用选择项?',
onOk() {
const body = { ids: selectedRowKeys.value, available: e.key == 1 ? true : false };
batchAvailableUser(body).then(response => {
const result = response.data;
message.success(result.msg);
selectedRowKeys.value = [];
loadingData();
}).catch(error => {
console.log(error);
})
}
});
}
// 表格分页
const handleTableChange = (values: any) => {
pagination.current = values.current;
pagination.pageSize = values.pageSize;
loadingData();
}
// 弹窗关键字处理
const modalHandle = (modalType: string, index?: number) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && index !== undefined) {
detailStateLoading.value = true;
detailState.value = dataSource.value[index];
detailState.value.index = index;
detailStateLoading.value = false;
} else if (modalType === 'update' && index !== undefined) {
const selected = dataSource.value[index];
Object.keys(updateState).forEach(key => {
if (selected[key] !== undefined) {
updateState[key] = selected[key];
}
})
if (selected['roles']) {
updateState.role_ids = selected.roles.map(role => role.id);
}
if (selected['positions']) {
updateState.position_ids = selected.positions.map(position => position.id);
}
}else if (modalType === 'import' && index !== undefined) {
}
}
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = {
username: createState.username,
name: createState.name,
password: createState.password,
dept_id: createState.dept_id,
role_ids: createState.role_ids,
position_ids: createState.position_ids,
gender: createState.gender,
email: createState.email,
mobile: createState.mobile,
available: createState.available,
description: createState.description,
is_superuser: createState.is_superuser
}
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
})
createUser(createBody).then(response => {
const result = response.data;
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error);
})
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error);
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
const updateBody = {
id: updateState.id,
username: updateState.username,
name: updateState.name,
dept_id: updateState.dept_id,
role_ids: updateState.role_ids,
position_ids: updateState.position_ids,
gender: updateState.gender,
email: updateState.email,
mobile: updateState.mobile,
available: updateState.available,
description: updateState.description,
is_superuser: updateState.is_superuser
}
if (showPasswordInput.value && updateState.password) {
updateBody['password'] = updateState.password;
}
updateUser(updateBody).then(response => {
const result = response.data;
modalSubmitLoading.value = false;
openModal.value = false;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
} else if (modalTitle.value === 'import') {
}
}
// 选择器弹窗
const selectModalHandle = (subject: string) => {
selectorModal.value.subject = subject;
selectorModal.value.openModal = true;
const ids_key = subject === 'role' ? 'role_ids' : 'position_ids';
const names_key = subject === 'role' ? 'roleNames' : 'positionNames';
const selectedKeys = modalTitle.value === 'create' ? createState[ids_key] : updateState[ids_key];
const selectedRowItemNames = modalTitle.value === 'create' ? (createState[names_key] ?? '') : (updateState[names_key] ?? '');
selectorModal.value.selectedRowKeys = selectedKeys;
selectorModal.value.selectedRowItemNames = selectedRowItemNames.split(', ');
selectorModal.value.loadingData();
}
// 选择器弹窗事件
const handleSelectorModalEvent = (
subject: string,
selectedSelectorRowKeys: roleSelectorType['id'][] | positionSelectorType['id'][],
selectedSelectorRowNames: roleSelectorType['name'][] | positionSelectorType['name'][]
) => {
const ids_key = subject === 'role' ? 'role_ids' : 'position_ids';
const names_key = subject === 'role' ? 'roleNames' : 'positionNames';
if (modalTitle.value === 'create') {
createState[ids_key] = selectedSelectorRowKeys;
createState[names_key] = selectedSelectorRowNames.join(', ');
} else {
updateState[ids_key] = selectedSelectorRowKeys;
updateState[names_key] = selectedSelectorRowNames.join(', ');
}
}
// 上传头像配置
const token = storage.get('Access-Token');
// 用户导入参数
const upload = reactive({
title: '用户导入',
open: false,
headers: token ? { Authorization: 'Bearer ' + token } : {},
url: importUserData, // 假设这是上传接口地址
// url: import.meta.env.VITE_APP_BASE_API + "/system/user/import/data",
isUploading: false,
updateSupport: false,
});
// 导出按钮操作
const handleExport = () => {
// 构建查询参数
const params = {
...queryState,
page_no: 1,
page_size: pagination.total // 导出所有数据
};
// 调用 exportLog 接口
exportUser(params).then(response => {
const blob = new Blob([response.data], { type: 'application/vnd.ms-excel' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `user_${new Date().getTime()}.xlsx`; // 设置下载文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
message.success('导出成功');
}).catch(error => {
console.error('导出失败:', error);
message.error('导出失败');
});
};
// 下载模板
const downloadTemplate = () => {
window.location.href = importUserTemplate; // 假设这是模板下载地址
};
// 导入
const beforeUpload = (file: File) => {
const isExcel = file.type === 'application/vnd.ms-excel' || file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
if (!isExcel) {
message.error('只能上传 Excel 文件!');
}
return isExcel;
};
const customRequest = ({ file, onSuccess, onError }: any) => {
upload.isUploading = true;
const formData = new FormData();
formData.append('file', file);
formData.append('updateSupport', upload.updateSupport.toString());
importUserData(formData)
.then((response) => {
upload.isUploading = false;
upload.open = false;
message.success('用户导入成功');
onSuccess(response);
loadingData(); // 刷新数据
})
.catch((error) => {
upload.isUploading = false;
onError(error);
message.error('用户导入失败');
});
};
/** 提交上传文件 */
const submitFileForm = () => {
uploadRef.value.upload();
};
</script>
<style lang="scss" scoped>
.table-search-wrapper {
margin-block-end: 16px;
}
</style>
+63
View File
@@ -0,0 +1,63 @@
export interface searchDataType {
username?: string
name?: string
available?: string
}
export interface searchSelectDataType {
name?: string
available?: string
}
export interface tableDataType {
id?: number;
index?: number;
username?: string;
name?: string;
email?: string;
mobile?: string;
gender?: number;
password?: string;
dept_id?: deptTreeType['id'];
dept_name?: deptTreeType['name'];
roles?: roleSelectorType[];
roleNames?: string;
role_ids?: roleSelectorType['id'][];
positions?: positionSelectorType[];
positionNames?: string;
position_ids?: positionSelectorType['id'][];
is_superuser?: boolean;
available?: boolean;
description?: string;
last_login?: string;
created_at?: string;
updated_at?: string;
creator?: creatorType;
}
export interface deptTreeType {
id?: number;
name?: string;
parent_id?: number;
children?: deptTreeType[];
}
export interface roleSelectorType {
id?: number;
name?: string;
available?: boolean;
description?: string;
}
export interface positionSelectorType {
id?: number;
name?: string;
available?: boolean;
description?: string;
}
interface creatorType {
id?: number;
name?: string;
username?: string;
}