init: 初始系统模板

This commit is contained in:
TsMask
2023-09-05 14:38:23 +08:00
parent a5bc16ae4f
commit 1075c8ae4f
130 changed files with 22531 additions and 1 deletions

View File

@@ -0,0 +1,248 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue/lib';
import { FileType } from 'ant-design-vue/lib/upload/interface';
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import { onMounted, reactive, ref, toRaw } from 'vue';
import { updateUserProfile, uploadAvatar } from '@/api/profile';
import { regExpEmail, regExpMobile, regExpNick } from '@/utils/regular-utils';
import useUserStore from '@/store/modules/user';
import useDictStore from '@/store/modules/dict';
const uerStore = useUserStore();
const { getDict } = useDictStore();
/**用户性别字典 */
let sysUserSex = ref<DictType[]>([
{ label: '未知', value: '0', elTagType: '', elTagClass: '' },
{ label: '男', value: '1', elTagType: '', elTagClass: '' },
{ label: '女', value: '2', elTagType: '', elTagClass: '' },
]);
/**表单数据状态 */
let stateForm = reactive({
/**表单属性 */
form: {
nickName: '',
email: '',
phonenumber: '',
sex: undefined,
},
/**表单提交点击状态 */
formClick: false,
});
/**表单数据状态初始化 */
function fnInitstateForm() {
stateForm.form = Object.assign(stateForm.form, uerStore.getBaseInfo);
stateForm.formClick = false;
}
/**表单验证通过 */
function fnFinish() {
Modal.confirm({
title: '提示',
content: `确认要提交修改用户基本信息吗?`,
onOk() {
stateForm.formClick = true;
// 发送请求
const hide = message.loading('请稍等...', 0);
const form = toRaw(stateForm.form);
updateUserProfile(form).then(res => {
hide();
stateForm.formClick = false;
if (res.code === 200) {
Modal.success({
title: '提示',
content: `用户基本信息修改成功!`,
okText: '我知道了',
onOk() {
uerStore.setBaseInfo(form);
},
});
} else {
message.error(`${res.msg}`, 3);
}
});
},
});
}
/**上传状态 */
let upState = ref<boolean>(false);
/**上传前检查或转换压缩 */
function fnBeforeUpload(file: FileType) {
if (upState.value) return false;
const isJpgOrPng = ['image/jpeg', 'image/png'].includes(file.type);
if (!isJpgOrPng) {
message.error('只支持上传图片格式jpg、png', 3);
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片文件大小必须小于 2MB', 3);
}
return isJpgOrPng && isLt2M;
}
/**上传变更 */
function fnUpload(up: UploadRequestOption) {
Modal.confirm({
title: '提示',
content: `确认要上传/变更用户头像吗?`,
onOk() {
// 发送请求
const hide = message.loading('请稍等...', 0);
upState.value = true;
let formData = new FormData();
formData.append('file', up.file);
uploadAvatar(formData).then(res => {
upState.value = false;
hide();
if (res.code === 200) {
message.success('头像上传/变更成功', 3);
uerStore.setAvatar(res.data);
} else {
message.error(res.msg, 3);
}
});
},
});
}
onMounted(() => {
// 初始字典数据
getDict('sys_user_sex').then(res => {
if (res.length > 0) {
sysUserSex.value = res;
}
});
// 初始表单值
fnInitstateForm();
});
</script>
<template>
<a-form
:model="stateForm.form"
name="stateForm"
layout="vertical"
:wrapper-col="{ span: 18 }"
@finish="fnFinish"
>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24" style="margin-bottom: 30px">
<a-form-item
label="用户昵称"
name="nickName"
:rules="[
{
required: true,
pattern: regExpNick,
message: '昵称只能包含字母、数字、中文和下划线且不少于2位',
},
]"
>
<a-input
v-model:value="stateForm.form.nickName"
allow-clear
:maxlength="26"
placeholder="请输入用户昵称"
></a-input>
</a-form-item>
<a-form-item
label="手机号码"
name="phonenumber"
:rules="[
{
required: false,
pattern: regExpMobile,
message: '请输入正确手机号码',
},
]"
>
<a-input
v-model:value="stateForm.form.phonenumber"
allow-clear
:maxlength="11"
placeholder="请输入手机号码"
></a-input>
</a-form-item>
<a-form-item
label="电子邮箱"
name="email"
:rules="[
{
required: false,
pattern: regExpEmail,
message: '请输入正确电子邮箱',
},
]"
>
<a-input
v-model:value="stateForm.form.email"
allow-clear
:maxlength="40"
placeholder="请输入电子邮箱"
></a-input>
</a-form-item>
<a-form-item
label="用户性别"
name="sex"
:rules="[
{
required: true,
message: '请选择用户性别',
},
]"
>
<a-select
v-model:value="stateForm.form.sex"
placeholder="用户性别"
:options="sysUserSex"
>
</a-select>
</a-form-item>
<a-space :size="8">
<a-button
block
type="primary"
html-type="submit"
:loading="stateForm.formClick"
>
确认修改
</a-button>
<a-button
type="default"
@click="fnInitstateForm"
:disabled="stateForm.formClick"
>
重置
</a-button>
</a-space>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-space direction="vertical" :size="16">
<a-image :width="128" :height="128" :src="uerStore.getAvatar" />
<span>请选择等比大小图片作为头像如200x200400x400</span>
<a-upload
name="file"
list-type="picture"
:max-count="1"
:show-upload-list="false"
:before-upload="fnBeforeUpload"
:custom-request="fnUpload"
>
<a-button type="default" :loading="upState">
上传/变更图片
</a-button>
</a-upload>
</a-space>
</a-col>
</a-row>
</a-form>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,158 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue/lib';
import { reactive } from 'vue';
import { updateUserPwd } from '@/api/profile';
import { regExpPasswd } from '@/utils/regular-utils';
import useUserStore from '@/store/modules/user';
import { useRouter } from 'vue-router';
const { userName, fnLogOut } = useUserStore();
const router = useRouter();
let state = reactive({
/**表单属性 */
form: {
oldPassword: '',
newPassword: '',
confirmPassword: '',
},
/**表单提交点击状态 */
formClick: false,
});
/**表单验证确认密码是否一致 */
function fnEqualToPassword(
rule: Record<string, any>,
value: string,
callback: (error?: string) => void
) {
if (!value) {
return Promise.reject('请输入确认新密码');
}
if (state.form.oldPassword === value) {
return Promise.reject('与旧密码一致,请重新输入新密码');
}
if (state.form.newPassword === value) {
return Promise.resolve();
}
return Promise.reject('两次输入的新密码不一致');
}
/**表单验证通过 */
function fnFinish() {
Modal.confirm({
title: '提示',
content: `确认要提交修改密码吗?`,
onOk() {
state.formClick = true;
// 发送请求
const hide = message.loading('请稍等...', 0);
updateUserPwd(state.form.oldPassword, state.form.confirmPassword)
.then(res => {
hide();
if (res.code === 200) {
Modal.success({
title: '提示',
content: `恭喜您,${userName} 账号密码修改成功!`,
okText: '重新登录',
onOk() {
fnLogOut().finally(() => router.push({ name: 'Login' }));
},
});
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
state.formClick = false;
});
},
});
}
</script>
<template>
<a-form
:model="state.form"
name="stateForm"
layout="vertical"
:wrapper-col="{ span: 9 }"
@finish="fnFinish"
>
<a-form-item
label="旧密码"
name="oldPassword"
:rules="[
{
required: true,
min: 6,
max: 26,
message: '旧密码不能为空且不少于6位',
},
]"
>
<a-input-password
v-model:value="state.form.oldPassword"
placeholder="请输入旧密码"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item
label="新密码"
name="newPassword"
:rules="[
{
required: true,
pattern: regExpPasswd,
message: '密码至少包含大小写字母、数字、特殊符号且不少于6位',
},
]"
>
<a-input-password
v-model:value="state.form.newPassword"
placeholder="请输入新密码"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item
label="确认新密码"
name="confirmPassword"
:rules="[
{
required: true,
validator: fnEqualToPassword,
},
]"
>
<a-input-password
v-model:value="state.form.confirmPassword"
placeholder="请确认新密码"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item :wrapper-col="{ span: 3 }">
<a-button
block
type="primary"
html-type="submit"
:loading="state.formClick"
>
提交修改
</a-button>
</a-form-item>
</a-form>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,165 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { getLocalColor, changePrimaryColor } from '@/hooks/useTheme';
import useLayoutStore from '@/store/modules/layout';
const { proConfig, changeConf } = useLayoutStore();
let color = ref<string>(getLocalColor());
/**改变主题色 */
function fnColorChange(e: Event) {
const target = e.target as HTMLInputElement;
if (target.nodeName === 'INPUT') {
changePrimaryColor(target.value ?? '#1890ff');
} else {
changePrimaryColor();
}
color.value = getLocalColor();
}
</script>
<template>
<a-divider orientation="left">布局属性</a-divider>
<a-list item-layout="vertical" size="large" row-key="title">
<a-list-item>
整体布局
<template #actions> 导航模式模块设置 </template>
<template #extra>
<a-radio-group
style="margin-bottom: 12px"
:value="proConfig.layout"
@change="(e:any) => changeConf('layout', e.target.value)"
>
<a-radio value="side">左侧菜单布局</a-radio>
<a-radio value="top">顶部菜单布局</a-radio>
<a-radio value="mix">混合菜单布局</a-radio>
</a-radio-group>
</template>
</a-list-item>
<a-list-item>
风格配色
<template #actions> 整体风格配色设置 </template>
<template #extra>
<a-space :size="16" align="end" direction="horizontal">
<a-button type="primary" size="small" @click="fnColorChange">
<BgColorsOutlined /> 随机
</a-button>
<input type="color" :value="color" @input="fnColorChange" />
</a-space>
</template>
</a-list-item>
<a-list-item>
深色菜单
<template #actions> 只能改变导航模式的菜单 </template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.navTheme === 'dark'"
@change="
(checked:any) => changeConf('navTheme', checked ? 'dark' : 'light')
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
固定顶部导航栏
<template #actions> 顶部导航栏是否固定不随滚动条移动 </template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.fixedHeader"
@change="(checked:any) => changeConf('fixedHeader', checked)"
></a-switch>
</template>
</a-list-item>
<a-list-item>
固定左侧菜单
<template #actions> 左侧菜单是否固定仅左侧菜单布局时有效 </template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.fixSiderbar"
@change="(checked:any) => changeConf('fixSiderbar', checked)"
></a-switch>
</template>
</a-list-item>
<a-list-item>
自动分割菜单
<template #actions>
顶部有多级菜单时显示左侧菜单仅混合菜单布局时有效
</template>
<template #extra>
<a-switch
checked-children=""
un-checked-children=""
:checked="proConfig.splitMenus"
@change="(checked:any) => changeConf('splitMenus', checked)"
></a-switch>
</template>
</a-list-item>
</a-list>
<a-divider orientation="left">内容区域</a-divider>
<a-list item-layout="vertical" size="large" row-key="title">
<a-list-item>
顶栏
<template #actions> 是否显示顶部导航栏 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.headerRender === undefined"
@change="
(checked:any) => changeConf('headerRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
页脚
<template #actions> 是否显示底部导航栏 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.footerRender === undefined"
@change="
(checked:any) => changeConf('footerRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
菜单头
<template #actions> 是否显示左侧菜单栏顶部LOGO区域 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.menuHeaderRender === undefined"
@change="
(checked:any) => changeConf('menuHeaderRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
<a-list-item>
导航标签项
<template #actions> 是否显示顶部Tab导航标签项 </template>
<template #extra>
<a-switch
checked-children="显示"
un-checked-children="隐藏"
:checked="proConfig.tabRender === undefined"
@change="
(checked:any) => changeConf('tabRender', checked === true && undefined)
"
></a-switch>
</template>
</a-list-item>
</a-list>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,198 @@
<script lang="ts" setup>
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message } from 'ant-design-vue/lib';
import { getUserProfile } from '@/api/profile';
import { reactive, ref, onMounted } from 'vue';
import { parseDateToStr } from '@/utils/date-utils';
import useUserStore from '@/store/modules/user';
/**加载状态 */
let loading = ref<boolean>(true);
/**Tab标签激活 */
let activeKey = ref<string>('list');
/**个人信息数据状态 */
let state = reactive<{
user: Record<string, any>;
postGroup: string[];
roleGroup: string[];
}>({
user: {},
postGroup: [],
roleGroup: [],
});
/**列表数据 */
let listData = ref([
{
id: 'Vue',
title: 'Vue.js - 渐进式 JavaScript 框架 | Vue.js',
description:
'基于标准 HTML、CSS 和 JavaScript 构建,提供容易上手的 API 和一流的文档。 性能出色 经过编译器优化、完全响应式的渲染系统,几乎不需要手动优化。',
},
{
id: 'Vue Router',
title: 'Vue Router | Vue.js 的官方路由',
description:
'为Vue.js 提供富有表现力、可配置的、方便的路由,用直观且强大的语法来定义静态或动态路由。',
},
{
id: 'Pinia',
title: 'Pinia | The intuitive store for Vue.js',
description:
'Pinia hooks into Vue devtools to give you an enhanced development experience in both Vue 2 and Vue 3. ',
},
{
id: 'Vite',
title: 'Vite | 下一代的前端工具链',
description:
'Vite(法语意为 "快速的",发音 /vit/,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验',
},
]);
/**查询用户个人信息 */
function fnGetProfile() {
getUserProfile().then(res => {
if (res.code === 200 && res.data) {
const { user, roleGroup, postGroup } = res.data;
state.user = user;
state.roleGroup = roleGroup;
state.postGroup = postGroup;
// 头像解析
state.user.avatar = useUserStore().fnAvatar(user.avatar);
loading.value = false;
} else {
message.error(res.msg, 3);
}
});
}
onMounted(() => {
// 获取信息
fnGetProfile();
});
</script>
<template>
<PageContainer :loading="loading">
<a-row :gutter="16">
<a-col :lg="6" :md="6" :xs="24">
<a-card :body-style="{ padding: '0px' }" style="margin-bottom: 16px">
<template #title>
<div class="info-top">
<div class="info-top-no">No{{ state.user.userId }}</div>
<a-avatar
shape="circle"
:size="96"
:src="state.user.avatar"
:alt="state.user.userName"
></a-avatar>
<div class="info-top-nickname" :title="state.user.nickName">
{{ state.user.nickName }}
</div>
</div>
</template>
<a-descriptions
size="small"
layout="vertical"
:bordered="true"
:column="1"
>
<a-descriptions-item label="手机号码">
{{ state.user.phonenumber || '-' }}
</a-descriptions-item>
<a-descriptions-item label="用户邮箱">
{{ state.user.email || '-' }}
</a-descriptions-item>
<a-descriptions-item label="所属部门">
{{ state.user.dept?.deptName || '-' }}
</a-descriptions-item>
<a-descriptions-item label="拥有岗位">
<span v-if="state.postGroup.length === 0">-</span>
<a-tag v-else v-for="v in state.postGroup" :key="v">
{{ v }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="拥有角色">
<span v-if="state.roleGroup.length === 0">-</span>
<a-tag v-else v-for="v in state.roleGroup" :key="v">
{{ v }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="登录地址">
{{ state.user.loginIp || '-' }}
</a-descriptions-item>
<a-descriptions-item label="登录时间">
<span v-if="+state.user.loginDate > 0">
{{ parseDateToStr(+state.user.loginDate) }}
</span>
<span v-else>-</span>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<a-col :lg="18" :md="18" :xs="24">
<a-card>
<a-tabs
tab-position="top"
:destroy-inactive-tab-pane="true"
v-model:activeKey="activeKey"
>
<a-tab-pane key="list" tab="列表">
<a-list
item-layout="horizontal"
:data-source="listData"
row-key="id"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
{{ item.title }}
</template>
<template #description>
{{ item.description }}
</template>
<template #avatar>
<a-avatar>{{ item.id }}</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-tab-pane>
<a-tab-pane key="empty" tab="空状态">
<a-empty>
<template #description> 暂无数据尝试刷新看看 </template>
<a-button type="primary">刷新</a-button>
</a-empty>
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped>
.info-top {
display: flex;
flex-direction: column;
align-items: center;
&-no {
align-self: flex-start;
font-size: 14px;
margin-bottom: 16px;
}
&-nickname {
margin-top: 16px;
font-size: 24px;
align-self: flex-start;
text-align: center;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import BaseInfo from './components/base-info.vue';
import ResetPasswd from './components/reset-passwd.vue';
import StyleLayout from './components/style-layout.vue';
/**Tab标签激活 */
let activeKey = ref<string>('base-info');
</script>
<template>
<PageContainer>
<a-card>
<a-tabs tab-position="left" v-model:activeKey="activeKey">
<a-tab-pane key="base-info" tab="基础信息">
<BaseInfo></BaseInfo>
</a-tab-pane>
<a-tab-pane key="reset-passwd" tab="重置密码">
<ResetPasswd></ResetPasswd>
</a-tab-pane>
<a-tab-pane key="style-layout" tab="个性化">
<StyleLayout></StyleLayout>
</a-tab-pane>
</a-tabs>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

27
src/views/dome/dome1.vue Normal file
View File

@@ -0,0 +1,27 @@
<template>
<PageContainer>
<a-result
status="404"
:style="{
height: '100%',
background: '#fff',
}"
title="Hello World"
sub-title="Sorry, you are not authorized to access this page."
>
<template #extra>
<a-button type="primary" @click="handleClick">Back Home</a-button>
</template>
</a-result>
</PageContainer>
</template>
<script lang="ts" setup>
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message } from 'ant-design-vue/lib';
const handleClick = () => {
console.log('info');
message.info('BackHome button clicked!');
};
</script>

61
src/views/dome/dome2.vue Normal file
View File

@@ -0,0 +1,61 @@
<template>
<a-layout class="layout">
<a-layout-header>
<div class="logo" />
<a-menu
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">nav 1</a-menu-item>
<a-menu-item key="2">nav 2</a-menu-item>
<a-menu-item key="3">nav 3</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout-content style="padding: 0 50px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>Home</a-breadcrumb-item>
<a-breadcrumb-item>List</a-breadcrumb-item>
<a-breadcrumb-item>App</a-breadcrumb-item>
</a-breadcrumb>
<div :style="{ background: '#fff', padding: '24px', minHeight: '280px' }">
Content {{ selectedKeys }}
</div>
</a-layout-content>
<a-layout-footer style="text-align: center">
Ant Design ©2018 Created by Ant UED
</a-layout-footer>
</a-layout>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const selectedKeys = ref<string[]>(['2']);
</script>
<style scoped>
.site-layout-content {
min-height: 280px;
padding: 24px;
background: #fff;
}
#components-layout-demo-top .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top .logo {
float: right;
margin: 16px 0 16px 24px;
}
[data-theme='dark'] .site-layout-content {
background: #141414;
}
</style>

30
src/views/dome/dome3.vue Normal file
View File

@@ -0,0 +1,30 @@
<template>
<PageContainer title="Version" sub-title="show current project dependencies">
<template #content>
<strong>Content Area</strong>
</template>
<template #extra>
<strong>Extra Area</strong>
</template>
<template #extraContent>
<strong>ExtraContent Area</strong>
</template>
<template #tags>
<a-tag>Tag1</a-tag>
<a-tag color="pink">Tag2</a-tag>
</template>
<a-card title="Info">
<p v-for="i in list" :key="i">
text block...
<a-tag>{{ i }}</a-tag>
</p>
</a-card>
</PageContainer>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
const list = ref<number>(50);
</script>

View File

@@ -0,0 +1,87 @@
<template>
<PageContainer :title="`${route.meta.title} ${route.params.id}`">
<template #content>
<a-descriptions size="small" :column="2">
<a-descriptions-item label="创建人">张三</a-descriptions-item>
<a-descriptions-item label="联系方式">
<a>421421</a>
</a-descriptions-item>
<a-descriptions-item label="创建时间">2017-01-10</a-descriptions-item>
<a-descriptions-item label="更新时间">2017-10-10</a-descriptions-item>
<a-descriptions-item label="备注">
中国浙江省杭州市西湖区古翠路
</a-descriptions-item>
</a-descriptions>
</template>
<template #extra>
<a-button key="3">操作</a-button>
<a-button key="2">操作</a-button>
<a-button key="1" type="primary">主操作</a-button>
</template>
<template #extraContent>
<a-space>
<a-statistic title="Feedback" :value="1128">
<template #prefix>
<LikeOutlined />
</template>
</a-statistic>
<a-statistic title="Unmerged" :value="93" suffix="/ 100" />
</a-space>
</template>
<!-- 主内容区 -->
<div style="height: 300px">
<p>路由参数联动 分页器 组件</p>
<a-space>
<a-button type="dashed" @click="prev">跳转上一页</a-button>
<a-button type="dashed" @click="next">跳转下一页</a-button>
</a-space>
<a-divider />
<a-pagination
:current="currentId"
:total="total"
show-less-items
@change="handlePageChange"
/>
</div>
</PageContainer>
</template>
<script setup lang="ts">
import { PageContainer } from '@ant-design-vue/pro-layout';
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const currentId = computed(() => {
let id = route.params && (route.params.id as string);
return Number.parseInt(id as string, 10) || 1;
});
const total = computed(() => {
const v = currentId.value * 20;
if (v >= Number.MAX_SAFE_INTEGER) {
return Number.MAX_SAFE_INTEGER;
}
return v;
});
const next = () => {
router.push({
name: 'DynamicMatch',
params: { id: currentId.value + 1 },
});
};
const prev = () => {
router.push({
name: 'DynamicMatch',
params: { id: currentId.value > 1 ? currentId.value - 1 : 1 },
});
};
const handlePageChange = (currentPage: number) => {
router.push({
name: 'DynamicMatch',
params: { id: currentPage },
});
};
</script>

View File

@@ -0,0 +1,58 @@
<template>
<PageContainer :title="title">
<template #content>
<a-descriptions size="small" :column="2">
<a-descriptions-item label="创建人">张三</a-descriptions-item>
<a-descriptions-item label="联系方式">
<a>421421</a>
</a-descriptions-item>
<a-descriptions-item label="创建时间">2017-01-10</a-descriptions-item>
<a-descriptions-item label="更新时间">2017-10-10</a-descriptions-item>
<a-descriptions-item label="备注">
中国浙江省杭州市西湖区古翠路
</a-descriptions-item>
</a-descriptions>
</template>
<template #extra>
<a-button key="3">操作</a-button>
<a-button key="2">操作</a-button>
<a-button key="1" type="primary">主操作</a-button>
</template>
<template #extraContent>
<a-space>
<a-statistic title="Feedback" :value="1128">
<template #prefix>
<LikeOutlined />
</template>
</a-statistic>
<a-statistic title="Unmerged" :value="93" suffix="/ 100" />
</a-space>
</template>
<!-- 主内容区 -->
<div style="height: 120vh">
<a-result
status="404"
:style="{
height: '100%',
background: '#fff',
}"
title="Hello World"
sub-title="Sorry, you are not authorized to access this page."
>
<template #extra>
<a-button type="primary">Back Home</a-button>
</template>
</a-result>
</div>
</PageContainer>
</template>
<script setup lang="ts">
import { PageContainer } from '@ant-design-vue/pro-layout';
import { ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
</script>

View File

@@ -0,0 +1,102 @@
<template>
<a-card>
<a-typography>
<a-typography-title>Introduction</a-typography-title>
<a-typography-paragraph>
In the process of internal desktop applications development, many
different design specs and implementations would be involved, which
might cause designers and developers difficulties and duplication and
reduce the efficiency of development.
</a-typography-paragraph>
<a-typography-paragraph>
After massive project practice and summaries, Ant Design, a design
language for background applications, is refined by Ant UED Team, which
aims to
<a-typography-text strong>
uniform the user interface specs for internal background projects,
lower the unnecessary cost of design differences and implementation
and liberate the resources of design and front-end development.
</a-typography-text>
</a-typography-paragraph>
<a-typography-title :level="2"
>Guidelines and Resources</a-typography-title
>
<a-typography-paragraph>
We supply a series of design principles, practical patterns and high
quality design resources (
<a-typography-text code>Sketch</a-typography-text>
and
<a-typography-text code>Axure</a-typography-text>
), to help people create their product prototypes beautifully and
efficiently.
</a-typography-paragraph>
<a-typography-paragraph>
<ul>
<li>
<a-typography-link href="/docs/resources"
>Resource Download</a-typography-link
>
</li>
</ul>
</a-typography-paragraph>
<a-typography-paragraph>
Press
<a-typography-text keyboard>Esc</a-typography-text>
to exit...
</a-typography-paragraph>
<a-divider />
<a-typography-title>介绍</a-typography-title>
<a-typography-paragraph>
蚂蚁的企业级产品是一个庞大且复杂的体系这类产品不仅量级巨大且功能复杂而且变动和并发频繁常常需要设计与开发能够快速的做出响应同时这类产品中有存在很多类似的页面以及组件可以通过抽象得到一些稳定且高复用性的内容
</a-typography-paragraph>
<a-typography-paragraph>
随着商业化的趋势越来越多的企业级产品对更好的用户体验有了进一步的要求带着这样的一个终极目标我们蚂蚁金服体验技术部经过大量的项目实践和总结逐步打磨出一个服务于企业级产品的设计体系
Ant Design基于
<a-typography-text mark>确定自然</a-typography-text>
的设计价值观通过模块化的解决方案降低冗余的生产成本让设计者专注于
<a-typography-text strong>更好的用户体验</a-typography-text>
</a-typography-paragraph>
<a-typography-title :level="2">设计资源</a-typography-title>
<a-typography-paragraph>
我们提供完善的设计原则最佳实践和设计资源文件
<a-typography-text code>Sketch</a-typography-text>
<a-typography-text code>Axure</a-typography-text>
来帮助业务快速设计出高质量的产品原型
</a-typography-paragraph>
<a-typography-paragraph>
<ul>
<li>
<a-typography-link href="/docs/resources-cn"
>设计资源</a-typography-link
>
</li>
</ul>
</a-typography-paragraph>
<a-typography-paragraph>
<blockquote>{{ blockContent }}</blockquote>
<pre>{{ blockContent }}</pre>
</a-typography-paragraph>
<a-typography-paragraph>
<a-typography-text keyboard>Esc</a-typography-text>
键退出阅读
</a-typography-paragraph>
</a-typography>
</a-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const blockContent = ref<string>(`AntV 是蚂蚁金服全新一代数据可视化解决方案致力于提供一套简单方便、专业可靠、不限可能的数据可视化最佳实践。得益于丰富的业务场景和用户需求挑战AntV 经历多年积累与不断打磨,已支撑整个阿里集团内外 20000+ 业务系统,通过了日均千万级 UV 产品的严苛考验。
我们正在基础图表,图分析,图编辑,地理空间可视化,智能可视化等各个可视化的领域耕耘,欢迎同路人一起前行。` );
</script>

21
src/views/error/403.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
</script>
<template>
<a-result
status="403"
title="没有访问权限"
sub-title="请不要进行非法操作您可以返回主页面或返回"
>
<template #extra>
<RouterLink :to="{ name: 'Index' }" :replace="true">
<a-button type="primary"> 返回首页 </a-button>
</RouterLink>
<a-button type="default" @click="() => router.back()"> 返回 </a-button>
</template>
</a-result>
</template>
<style lang="less" scoped></style>

26
src/views/error/404.vue Normal file
View File

@@ -0,0 +1,26 @@
<script setup lang="ts"></script>
<template>
<a-result
status="404"
title="找不到匹配页面"
sub-title="对不起您正在寻找的页面不存在"
>
<template #extra>
<RouterLink :to="{ name: 'Index' }" :replace="true">
<a-button type="primary"> 返回首页 </a-button>
</RouterLink>
</template>
<a-typography>
<a-typography-title> 找不到网页 </a-typography-title>
<a-typography-paragraph>
1. 尝试检查URL的错误然后按浏览器上的刷新按钮
</a-typography-paragraph>
<a-typography-paragraph>
2. 尝试在我们的应用程序中找到其他内容
</a-typography-paragraph>
</a-typography>
</a-result>
</template>
<style lang="less" scoped></style>

123
src/views/index.vue Normal file
View File

@@ -0,0 +1,123 @@
<script setup lang="ts">
import donate from '@/assets/donate.jpg';
import { PageContainer } from '@ant-design-vue/pro-layout';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
const userStore = useUserStore();
const { appName, appVersion } = useAppStore();
/**跳转 */
function goTarget(type: string) {
let url = '';
if (type === 'code') {
url = 'https://gitee.com/TsMask/';
}
if (type === 'issues') {
url = 'https://gitee.com/TsMask/mask_antd_vue3/issues';
}
window.open(url, '__blank');
}
</script>
<template>
<PageContainer :breadcrumb="false" :title="appName" sub-title="by TsMask">
<template #tags>
<a-tag>当前版本{{ appVersion }}</a-tag>
<a-tag color="magenta"><PayCircleOutlined /> 免费开源</a-tag>
</template>
<template #extra>
<a-button type="primary" @click="goTarget('code')">开源仓库</a-button>
<a-button type="default" @click="goTarget('issues')">提些建议</a-button>
</template>
<template #content>
<a-space :size="16" align="center">
<a-avatar
shape="circle"
:size="72"
:src="userStore.getAvatar"
:alt="userStore.userName"
></a-avatar>
<span class="nickname">
{{ userStore.nickName }} 想必你那里一切安好吧
</span>
</a-space>
</template>
<template #extraContent>
<a-space :size="16">
<a-statistic title="在线用户" :value="545486" />
</a-space>
</template>
<a-row :gutter="16">
<a-col :lg="16" :md="16" :xs="24">
<a-card title="项目简介" style="margin-bottom: 16px">
<a-typography>
<a-typography-paragraph>
<a-typography-text mark> Vue3 </a-typography-text>
技术组合支持按钮及数据权限可自定义部门数据权限
</a-typography-paragraph>
<a-typography-paragraph>
内置模块如部门管理角色用户菜单及按钮授权数据权限系统参数日志管理等支持在线定时任务配置
</a-typography-paragraph>
<a-typography-paragraph>
使用 <a-typography-text mark> Ant-Design-Vue </a-typography-text>
组件库搭建的前后端分离极速后台管理系统
</a-typography-paragraph>
</a-typography>
</a-card>
</a-col>
<a-col :lg="8" :md="8" :xs="24">
<a-card title="快速开始" style="margin-bottom: 16px">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-button
type="link"
target="_blank"
title="开发手册"
href="https://juejin.cn/column/7188761626017792056"
>
开发手册
</a-button>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-button
type="link"
target="_blank"
title="来自Apifox的接口文档"
href="https://mask-api-midwayjs.apifox.cn/"
>
接口文档
</a-button>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-button
type="link"
target="_blank"
title="Middwayjs版本服务端"
href="https://gitee.com/TsMask/mask_api_midwayjs"
>
Node后端
</a-button>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-button type="text"> 相关待定 </a-button>
</a-col>
</a-row>
</a-card>
<a-card title="捐赠鼓励" style="margin-top: 16px">
<a-image width="100%" :src="donate" />
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped>
.nickname {
margin-bottom: 12px;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
font-size: 20px;
line-height: 28px;
}
</style>

469
src/views/login.vue Normal file
View File

@@ -0,0 +1,469 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue/lib';
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
import useUserStore from '@/store/modules/user';
import { getCaptchaImage } from '@/api/login';
import useI18n from '@/hooks/useI18n';
import { regExpMobile, validMobile } from '@/utils/regular-utils';
import { useRouter, useRoute } from 'vue-router';
const { t, changeLocale } = useI18n();
const router = useRouter();
const route = useRoute();
const codeImgFall =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
/**登录后重定向页面 */
const redirectPath =
(route.query && (route.query.redirect as string)) || '/index';
/**Tab默认激活 */
let activeKey = ref<'username' | 'phonenumber'>('username');
let state = reactive({
/**表单属性 */
from: {
/**账号 */
username: 'maskAdmin',
/**密码 */
password: 'Admin@1234',
/**手机号 */
phonenumber: '',
/**验证码 */
code: '',
/**验证码uuid */
uuid: '',
},
/**表单提交点击状态 */
fromClick: false,
/**验证码状态 */
captcha: {
/**验证码开关 */
enabled: false,
/**验证码图片地址 */
codeImg: '',
codeImgFall: codeImgFall,
},
/**验证码点击状态 */
captchaClick: false,
});
/**表单验证通过 */
function fnFinish() {
state.fromClick = true;
let form = {};
// 账号密码登录
if (activeKey.value === 'username') {
form = {
username: state.from.username,
password: state.from.password,
code: state.from.code,
uuid: state.from.uuid,
};
}
// 手机号登录
if (activeKey.value === 'phonenumber') {
form = {
phonenumber: state.from.phonenumber,
code: state.from.code,
uuid: state.from.uuid,
};
}
// 发送请求
useUserStore()
.fnLogin(form)
.then(res => {
if (res.code === 200) {
message.success(t('views.login.loginSuccess'), 3);
router.push({ path: redirectPath });
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
state.fromClick = false;
// 刷新验证码
if (state.captcha.enabled) {
state.from.code = '';
fnGetCaptcha();
}
});
}
/**
* 获取验证码
*/
function fnGetCaptcha() {
if (state.captchaClick) return;
state.captchaClick = true;
getCaptchaImage().then(res => {
state.captchaClick = false;
if (res.code != 200) {
message.warning(`${res.msg}`, 3);
return;
}
state.captcha.enabled = Boolean(res.captchaEnabled);
if (state.captcha.enabled) {
state.captcha.codeImg = res.img;
state.from.uuid = res.uuid;
}
});
}
/**短信验证码定时器 */
let smsInterval: any = undefined;
/**短信验证码信息状态 */
let smsState = reactive({
/**点击状态 */
click: false,
/**发送倒计时 */
time: 120,
});
/**获取短信验证码 */
function fnGetSmsCaptcha() {
if (smsState.click) return;
if (!validMobile(state.from.phonenumber)) {
message.warning(t('valid.phoneReg'), 3);
return;
}
smsState.click = true;
setTimeout(() => {
// start 得到发送结果启动定时
message.success(t('valid.codeSmsSend'), 3);
state.from.uuid = '短信校验id';
smsInterval = setInterval(() => {
if (smsState.time <= 0) {
smsState.time = 120;
smsState.click = false;
clearTimeout(smsInterval);
} else {
smsState.time--;
}
}, 1000);
// end
}, 1000);
}
/**改变多语言 */
function fnChangeLocale(e: any) {
changeLocale(e.key);
}
onMounted(() => {
fnGetCaptcha();
});
onBeforeUnmount(() => {
smsState.time = 120;
smsState.click = false;
clearTimeout(smsInterval);
});
</script>
<template>
<div class="container">
<div class="top">
<div class="header">
<a href="/" target="_self"
><img src="@/assets/logo.png" class="logo" alt="logo" />
<span class="title">{{ t('common.title') }}</span>
</a>
</div>
<div class="desc">{{ t('common.desc') }}</div>
</div>
<div class="main">
<a-form :model="state.from" name="stateFrom" @finish="fnFinish">
<a-tabs
v-model:activeKey="activeKey"
tabPosition="top"
type="line"
:centered="true"
:destroy-inactive-tab-pane="true"
>
<a-tab-pane key="username" :tab="t('views.login.tabPane1')">
<a-form-item
name="username"
:rules="[
{
required: true,
min: 2,
max: 30,
message: t('valid.userNamePlease'),
},
]"
>
<a-input
v-model:value="state.from.username"
size="large"
:placeholder="t('valid.userNameHit')"
:maxlength="30"
>
<template #prefix>
<UserOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[
{
required: true,
min: 6,
max: 26,
message: t('valid.passwordPlease'),
},
]"
>
<a-input-password
v-model:value="state.from.password"
size="large"
:placeholder="t('valid.passwordHit')"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-row :gutter="8" v-if="state.captcha.enabled">
<a-col :span="16">
<a-form-item
name="code"
:rules="[
{
required: true,
min: 1,
message: t('valid.codePlease'),
},
]"
>
<a-input
v-model:value="state.from.code"
size="large"
:placeholder="t('valid.codeHit')"
:maxlength="6"
>
<template #prefix>
<RobotOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="8">
<a-image
:alt="t('valid.codeHit')"
style="cursor: pointer; border-radius: 2px"
width="120px"
height="40px"
:preview="false"
:src="state.captcha.codeImg"
:fallback="state.captcha.codeImgFall"
@click="fnGetCaptcha"
/>
</a-col>
</a-row>
<a-row
:gutter="8"
justify="space-between"
align="middle"
style="margin-bottom: 16px"
>
<a-col :span="12">
<a-button
type="link"
target="_self"
:title="t('views.login.registerBtn')"
@click="() => router.push({ name: 'Register' })"
>
{{ t('views.login.registerBtn') }}
</a-button>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="phonenumber" :tab="t('views.login.tabPane2')">
<a-form-item
name="phonenumber"
:rules="[
{
required: true,
pattern: regExpMobile,
message: t('valid.phonePlease'),
},
]"
>
<a-input
v-model:value="state.from.phonenumber"
size="large"
:placeholder="t('valid.phoneHit')"
:maxlength="11"
>
<template #prefix>
<MobileOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="code"
:rules="[
{
required: true,
min: 4,
message: t('valid.codePlease'),
},
]"
>
<a-input
v-model:value="state.from.code"
size="large"
:placeholder="t('valid.codeHit')"
:maxlength="6"
>
<template #prefix>
<RobotOutlined class="prefix-icon" />
</template>
<template #suffix>
<a-button
size="small"
type="link"
:disabled="smsState.click"
@click="fnGetSmsCaptcha"
>
{{
smsState.click
? `${smsState.time} s`
: t('valid.codeText')
}}
</a-button>
</template>
</a-input>
</a-form-item>
</a-tab-pane>
</a-tabs>
<a-button
block
type="primary"
size="large"
html-type="submit"
:loading="state.fromClick"
>
{{ t('views.login.loginBtn') }}
</a-button>
<a-row
:gutter="8"
justify="space-between"
align="middle"
style="margin-top: 18px"
>
<a-col :span="18">
<span>{{ t('views.login.loginMethod') }}</span>
<a-tooltip :title="t('views.login.loginMethodWX')">
<a-button shape="circle" size="middle" type="link">
<template #icon>
<WechatOutlined
:style="{ color: '#52c41a', fontSize: '18px' }"
/>
</template>
</a-button>
</a-tooltip>
<a-tooltip :title="t('views.login.loginMethodQQ')">
<a-button shape="circle" size="middle" type="link">
<template #icon>
<QqOutlined :style="{ color: '#40a9ff', fontSize: '18px' }" />
</template>
</a-button>
</a-tooltip>
</a-col>
<a-col :span="6">
<a-dropdown :trigger="['click', 'hover']">
<a-button size="small" type="default">
{{ t('i18n') }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="fnChangeLocale">
<a-menu-item key="zh_CN">中文</a-menu-item>
<a-menu-item key="en_US">English</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-col>
</a-row>
</a-form>
</div>
</div>
</template>
<style lang="less" scoped>
.container {
position: relative;
width: 100%;
min-height: 100%;
padding: 110px 0 144px;
background-image: url(../assets/background.svg);
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.top {
text-align: center;
a {
text-decoration: none;
}
.header {
height: 44px;
line-height: 44px;
.logo {
height: 44px;
margin-right: 16px;
vertical-align: top;
border-style: none;
border-radius: 6.66px;
}
.title {
position: relative;
top: 2px;
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
font-size: 33px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
}
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
.main {
width: 368px;
min-width: 260px;
margin: 0 auto;
.prefix-icon {
color: #8c8c8c;
font-size: 16px;
}
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
}
</style>

498
src/views/monitor/cache/index.vue vendored Normal file
View File

@@ -0,0 +1,498 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted } from 'vue';
import {
listCacheName,
listCacheKey,
getCacheValue,
clearCacheName,
clearCacheKey,
clearCacheSafe,
} from '@/api/monitor/cache';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { ColumnsType } from 'ant-design-vue/lib/table/Table';
import { message } from 'ant-design-vue/lib';
import { hasPermissions } from '@/plugins/auth-user';
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**请求点击 */
let isClick = ref<boolean>(false);
/**缓存内容信息 */
let cacheKeyInfo = reactive({
loading: true,
data: {
cacheKey: '',
cacheName: '',
cacheValue: '',
remark: '',
},
});
/**
* 查询缓存内容详细
* @param cacheKey
*/
function fnCacheKeyInfo(cacheKey: string) {
if (!hasPermissions(['monitor:cache:query'])) return;
if (isClick.value) return;
isClick.value = true;
cacheKeyInfo.loading = true;
getCacheValue(cacheKeyTable.cacheName, cacheKey).then(res => {
isClick.value = false;
if (res.code === 200) {
cacheKeyInfo.data = Object.assign(cacheKeyInfo.data, res.data);
cacheKeyInfo.loading = false;
}
});
}
/**键名列表表格字段列 */
let cacheKeyTableColumns: ColumnsType = [
{
title: '序号',
dataIndex: 'num',
width: '50px',
align: 'center',
customRender(opt) {
return opt.index + 1;
},
},
{
title: '缓存键名',
dataIndex: 'cacheKey',
align: 'left',
ellipsis: true,
// 渲染值处理
customRender(opt) {
return opt.text;
},
// 自定义过滤控件
customFilterDropdown: true,
onFilter: (value, record) => {
if (typeof value === 'string') {
const nameLower = record.cacheKey.toLowerCase();
return nameLower.includes(value.toLowerCase());
}
},
},
{
title: '操作',
key: 'option',
align: 'center',
width: '50px',
},
];
/**键名列表表格数据 */
let cacheKeyTable = reactive({
loading: true,
data: [],
/**当前键名列表的缓存名称 */
cacheName: '',
});
/**
* 清理指定缓存键名
* @param cacheKey 键名列表中的缓存键名
*/
function fnCacheKeyClear(cacheKey: string) {
if (isClick.value) return;
isClick.value = true;
const hide = message.loading('请稍等...', 0);
clearCacheKey(cacheKeyTable.cacheName, cacheKey).then(res => {
hide();
isClick.value = false;
if (res.code === 200) {
message.success({
content: `已删除缓存键名 ${cacheKey}`,
duration: 3,
});
// 缓存内容显示且是删除的缓存键名,需要进行加载显示
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheKey === cacheKey) {
cacheKeyInfo.loading = true;
}
} else {
message.error({
content: res.msg,
duration: 3,
});
}
fnCacheKeyList();
});
}
/** 查询缓存键名列表 */
function fnCacheKeyList(cacheName: string = 'load') {
if (cacheName === 'load') {
cacheName = cacheKeyTable.cacheName;
}
if (!cacheName) {
message.warning('请在缓存列表中选择数据项!', 3);
return;
}
if (isClick.value) return;
isClick.value = true;
cacheKeyTable.loading = true;
listCacheKey(cacheName).then(res => {
isClick.value = false;
if (res.code === 200 && res.data) {
cacheKeyTable.cacheName = cacheName;
cacheKeyTable.data = res.data;
cacheKeyTable.loading = false;
}
});
}
/**缓存列表表格数据 */
let cacheNameTable = reactive({
loading: true,
data: [],
});
/**缓存列表表格字段列 */
let cacheNameTableColumns: ColumnsType = [
{
title: '序号',
dataIndex: 'num',
width: '50px',
align: 'center',
customRender(opt) {
return opt.index + 1;
},
},
{
title: '缓存名称',
dataIndex: 'cacheName',
align: 'left',
ellipsis: true,
// 渲染值处理
customRender(opt) {
return opt.text;
},
// 自定义过滤控件
customFilterDropdown: true,
onFilter: (value, record) => {
if (typeof value === 'string') {
const nameLower = record.cacheName.toLowerCase();
return nameLower.includes(value.toLowerCase());
}
},
},
{
title: '备注',
dataIndex: 'remark',
align: 'left',
ellipsis: true,
},
{
title: '操作',
key: 'option',
align: 'center',
width: '50px',
},
];
/**安全清理缓存 */
function fnClearCacheSafe() {
if (isClick.value) return;
isClick.value = true;
const hide = message.loading('请稍等...', 0);
clearCacheSafe().then(res => {
hide();
isClick.value = false;
if (res.code === 200) {
message.success({
content: '已完成安全清理缓存',
duration: 3,
});
cacheKeyTable.loading = true;
cacheKeyInfo.loading = true;
} else {
message.error({
content: res.msg,
duration: 3,
});
}
});
}
/**
* 清理指定缓存名称
* @param cacheName 缓存名称
*/
function fnCacheNameClear(cacheName: string) {
if (isClick.value) return;
isClick.value = true;
const hide = message.loading('请稍等...', 0);
clearCacheName(cacheName).then(res => {
hide();
isClick.value = false;
if (res.code === 200) {
message.success({
content: `已清理缓存名称 ${cacheName}`,
duration: 3,
});
// 缓存内容显示且是删除的缓存名称,需要进行加载显示
if (!cacheKeyInfo.loading && cacheKeyInfo.data.cacheName === cacheName) {
cacheKeyInfo.loading = true;
}
} else {
message.error({
content: res.msg,
duration: 3,
});
}
fnCacheKeyList(cacheName);
});
}
/**查询缓存名称列表 */
function fnCacheNameList() {
if (isClick.value) return;
isClick.value = true;
cacheNameTable.loading = true;
listCacheName().then(res => {
isClick.value = false;
if (res.code === 200 && res.data) {
cacheNameTable.data = res.data;
cacheNameTable.loading = false;
}
});
}
onMounted(() => {
fnCacheNameList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph>
系统在缓存
<a-typography-text code>Redis</a-typography-text>
应用程序中的可控的缓存信息
</a-typography-paragraph>
</template>
<a-row :gutter="20">
<a-col :lg="8" :md="8" :xs="24">
<a-card
title="缓存列表"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnCacheNameList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>安全清理</template>
<a-popconfirm
placement="bottomRight"
title="确认要执行可安全清理的缓存下所有键名吗?`"
ok-text="确认"
cancel-text="取消"
@confirm="fnClearCacheSafe()"
>
<a-button type="text" v-perms:has="['monitor:cache:remove']">
<template #icon><ClearOutlined /></template>
</a-button>
</a-popconfirm>
</a-tooltip>
</a-space>
</template>
<a-table
row-key="cacheName"
size="small"
:columns="cacheNameTableColumns"
:data-source="cacheNameTable.data"
:loading="cacheNameTable.loading"
:row-selection="{
type: 'radio',
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyList(selectedRowKeys[0] as string),
}"
:pagination="false"
>
<template
#customFilterDropdown="{
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
column,
}"
>
<div style="padding: 8px">
<a-input
:placeholder="`模糊过滤 ${column.title}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block"
@change="
e => setSelectedKeys(e.target.value ? [e.target.value] : [])
"
@pressEnter="confirm()"
/>
<a-button
type="primary"
size="small"
style="width: 90px; margin-right: 8px"
@click="confirm()"
>
过滤
</a-button>
<a-button
size="small"
style="width: 90px"
@click="clearFilters({ confirm: true })"
>
重置
</a-button>
</div>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'option'">
<a-popconfirm
placement="topRight"
title="确认要清理该缓存名称下的所有键名吗?`"
ok-text="确认"
cancel-text="取消"
@confirm="fnCacheNameClear(record.cacheName)"
>
<a-button type="text" v-perms:has="['monitor:cache:remove']">
<template #icon><ClearOutlined /></template>
</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</a-card>
</a-col>
<a-col :lg="8" :md="8" :xs="24">
<a-card
title="键名列表"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<template #extra>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnCacheKeyList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
</template>
<a-table
row-key="cacheKey"
size="small"
:columns="cacheKeyTableColumns"
:data-source="cacheKeyTable.data"
:loading="cacheKeyTable.loading"
:row-selection="{
type: 'radio',
onChange: (selectedRowKeys: (string|number)[]) => fnCacheKeyInfo(selectedRowKeys[0] as string),
}"
:pagination="false"
>
<template
#customFilterDropdown="{
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
column,
}"
>
<div style="padding: 8px">
<a-input
:placeholder="`模糊过滤 ${column.title}`"
:value="selectedKeys[0]"
style="width: 188px; margin-bottom: 8px; display: block"
@change="
e => setSelectedKeys(e.target.value ? [e.target.value] : [])
"
@pressEnter="confirm()"
/>
<a-button
type="primary"
size="small"
style="width: 90px; margin-right: 8px"
@click="confirm()"
>
过滤
</a-button>
<a-button
size="small"
style="width: 90px"
@click="clearFilters({ confirm: true })"
>
重置
</a-button>
</div>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'option'">
<a-popconfirm
placement="topRight"
title="确认要删除该缓存键吗?`"
ok-text="确认"
cancel-text="取消"
@confirm="fnCacheKeyClear(record.cacheKey)"
>
<a-button type="text" v-perms:has="['monitor:cache:remove']">
<template #icon><DeleteOutlined /></template>
</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</a-card>
</a-col>
<a-col :lg="8" :md="8" :xs="24" v-perms:has="['monitor:cache:query']">
<a-card
title="缓存内容"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
:loading="cacheKeyInfo.loading"
>
<a-descriptions
size="small"
layout="vertical"
:bordered="true"
:column="1"
>
<a-descriptions-item label="缓存名称">
{{ cacheKeyInfo.data.cacheName }}
</a-descriptions-item>
<a-descriptions-item label="缓存键名">
{{ cacheKeyInfo.data.cacheKey }}
</a-descriptions-item>
<a-descriptions-item label="缓存内容">
<a-typography-paragraph>
<a-textarea
:value="cacheKeyInfo.data.cacheValue"
:auto-size="{ minRows: 4, maxRows: 10 }"
:maxlength="4000"
:disabled="true"
placeholder="显示缓存内容"
/>
</a-typography-paragraph>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped></style>

222
src/views/monitor/cache/info.vue vendored Normal file
View File

@@ -0,0 +1,222 @@
<script setup lang="ts">
import * as echarts from 'echarts/core';
import {
ToolboxComponent,
ToolboxComponentOption,
TooltipComponent,
TooltipComponentOption,
LegendComponent,
LegendComponentOption,
} from 'echarts/components';
import { PieChart, PieSeriesOption } from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { getCache } from '@/api/monitor/cache';
import { reactive, ref, onMounted } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
echarts.use([
ToolboxComponent,
TooltipComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout,
]);
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**加载状态 */
let loading = ref<boolean>(true);
/**数据参数类型 */
type CacheType = {
/**服务信息 */
info: InfoType;
/**当前连接可用键Key总数 */
dbSize: number;
/**命令状态 */
commandStats: Record<string, string>[];
};
/**数据参数服务信息类型 */
type InfoType = {
clients: Record<string, string>;
cluster: Record<string, string>;
cpu: Record<string, string>;
errorstats: Record<string, string>;
keyspace: Record<string, string>;
memory: Record<string, string>;
modules: Record<string, string>;
persistence: Record<string, string>;
replication: Record<string, string>;
server: Record<string, string>;
stats: Record<string, string>;
};
let cache: CacheType = reactive({
info: {
clients: {},
cluster: {},
cpu: {},
errorstats: {},
keyspace: {},
memory: {},
modules: {},
persistence: {},
replication: {},
server: {},
stats: {},
},
dbSize: 0,
commandStats: [],
});
/**生成命令统计图 */
function commandStatsChart() {
const commandStatsDom = document.getElementById('commandstats');
if (!commandStatsDom) return;
const commandStatsEchart = echarts.init(commandStatsDom);
const option: echarts.ComposeOption<
| ToolboxComponentOption
| TooltipComponentOption
| LegendComponentOption
| PieSeriesOption
> = {
// 鼠标悬浮提示
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
// 左侧标签
legend: {
orient: 'vertical',
left: 'left',
},
// 右侧工具
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true },
},
},
series: [
{
name: '命令',
type: 'pie',
radius: ['5%', '80%'],
center: ['60%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8,
},
data: cache.commandStats,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
};
commandStatsEchart.setOption(option);
window.addEventListener('resize', function () {
commandStatsEchart.resize();
});
}
onMounted(() => {
getCache()
.then(res => {
if (res.code === 200 && res.data) {
cache.info = res.data.info;
cache.dbSize = res.data.dbSize;
cache.commandStats = res.data.commandStats;
// 加载状态
loading.value = false;
}
})
.then(() => {
// 加载结束后生成图
commandStatsChart();
});
});
</script>
<template>
<PageContainer :title="title" :loading="loading">
<template #content>
<a-typography-paragraph>
缓存
<a-typography-text code>Redis</a-typography-text>
应用程序的信息
</a-typography-paragraph>
</template>
<a-card
title="基本信息"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<a-descriptions
size="middle"
layout="horizontal"
:label-style="{ width: '140px' }"
:bordered="true"
:column="{ lg: 4, md: 2, xs: 1 }"
>
<a-descriptions-item label="Redis版本">
{{ cache.info.server.redis_version }}
</a-descriptions-item>
<a-descriptions-item label="运行模式">
{{ cache.info.server.redis_mode == 'standalone' ? '单机' : '集群' }}
</a-descriptions-item>
<a-descriptions-item label="端口">
{{ cache.info.server.tcp_port }}
</a-descriptions-item>
<a-descriptions-item label="客户端数">
{{ cache.info.clients.connected_clients }}
</a-descriptions-item>
<a-descriptions-item label="运行时间(天)">
{{ cache.info.server.uptime_in_days }}
</a-descriptions-item>
<a-descriptions-item label="使用内存">
{{ cache.info.memory.used_memory_human }}
</a-descriptions-item>
<a-descriptions-item label="使用CPU">
{{ parseFloat(cache.info.cpu.used_cpu_user_children).toFixed(2) }}
</a-descriptions-item>
<a-descriptions-item label="内存配置">
{{ cache.info.memory.maxmemory_human }}
</a-descriptions-item>
<a-descriptions-item label="AOF是否开启">
{{ cache.info.persistence.aof_enabled == '0' ? '否' : '是' }}
</a-descriptions-item>
<a-descriptions-item label="RDB是否成功">
{{ cache.info.persistence.rdb_last_bgsave_status }}
</a-descriptions-item>
<a-descriptions-item label="Key数量">
{{ cache.dbSize }}
</a-descriptions-item>
<a-descriptions-item label="网络入口/出口">
{{ cache.info.stats.instantaneous_input_kbps }} kps /
{{ cache.info.stats.instantaneous_output_kbps }} kps
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="命令统计" :bordered="false">
<div id="commandstats" style="height: 400px; width: 100%"></div>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,682 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
exportJobLog,
listJobLog,
delJobLog,
cleanJobLog,
} from '@/api/monitor/jobLog';
import { getJob } from '@/api/monitor/job';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useTabsStore from '@/store/modules/tabs';
import useDictStore from '@/store/modules/dict';
const tabsStore = useTabsStore();
const { getDict } = useDictStore();
const route = useRoute();
const router = useRouter();
// 获取地址栏参数
const jobId = route.params && (route.params.jobId as string);
/**字典数据 */
let dict: {
/**任务组名 */
sysJobGroup: DictType[];
/**执行状态 */
sysCommonStatus: DictType[];
} = reactive({
sysJobGroup: [],
sysCommonStatus: [],
});
/**记录开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**任务名称 */
jobName: '',
/**任务组名 */
jobGroup: undefined,
/**执行状态 */
status: undefined,
/**记录开始时间 */
beginTime: '',
/**记录结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
if (jobId && jobId !== '0') {
queryParams = Object.assign(queryParams, {
status: undefined,
beginTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
} else {
queryParams = Object.assign(queryParams, {
jobName: '',
jobGroup: undefined,
status: undefined,
beginTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
}
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '日志编号',
dataIndex: 'jobLogId',
align: 'center',
},
{
title: '任务名称',
dataIndex: 'jobName',
align: 'center',
},
{
title: '任务组名',
dataIndex: 'jobGroup',
key: 'jobGroup',
align: 'center',
},
{
title: '调用目标',
dataIndex: 'invokeTarget',
align: 'center',
},
{
title: '执行状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '记录时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '消耗时间',
dataIndex: 'costTime',
key: 'costTime',
align: 'center',
customRender(opt) {
return `${opt.value} ms`;
},
},
{
title: '操作',
key: 'jobLogId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
title: '任务日志',
from: {
jobLogId: undefined,
jobName: '',
jobGroup: 'DEFAULT',
invokeTarget: '',
targetParams: '',
status: '0',
jobMsg: '',
createTime: 0,
},
});
/**
* 对话框弹出显示为 查看
* @param row 调度日志信息对象
*/
function fnModalVisibleByVive(row: Record<string, string>) {
modalState.from = Object.assign(modalState.from, row);
modalState.title = '调度日志信息';
modalState.visibleByView = true;
}
/**
* 对话框弹出关闭执行函数
*/
function fnModalCancel() {
modalState.visibleByView = false;
}
/**
* 任务删除
*/
function fnRecordDelete() {
const ids = tableState.selectedRowKeys.join(',');
Modal.confirm({
title: '提示',
content: `确认删除调度日志编号为 【${ids}】 的数据项吗?`,
onOk() {
const key = 'delJobLog';
message.loading({ content: '请稍等...', key });
delJobLog(ids).then(res => {
if (res.code === 200) {
message.success({
content: `删除成功`,
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**列表清空 */
function fnCleanList() {
Modal.confirm({
title: '提示',
content: `确认清空所有调度日志数据项吗?`,
onOk() {
const key = 'cleanJobLog';
message.loading({ content: '请稍等...', key });
cleanJobLog().then(res => {
if (res.code === 200) {
message.success({
content: `清空成功`,
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: '提示',
content: `确认根据搜索条件导出xlsx表格文件吗?`,
onOk() {
const key = 'exportJobLog';
message.loading({ content: '请稍等...', key });
exportJobLog(toRaw(queryParams)).then(res => {
if (res.code === 200) {
message.success({
content: `已完成导出`,
key,
duration: 2,
});
saveAs(res.data, `job_log_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**关闭跳转 */
function fnClose() {
const to = tabsStore.tabClose(route.path);
if (to) {
router.push(to);
} else {
router.back();
}
}
/**查询调度日志列表 */
function fnGetList() {
tableState.loading = true;
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listJobLog(toRaw(queryParams)).then(res => {
if (res.code === 200) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
tableState.loading = false;
}
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('sys_job_group'),
getDict('sys_common_status'),
]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysJobGroup = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.sysCommonStatus = resArr[1].value;
}
});
// 指定任务id数据列表
if (jobId && jobId !== '0') {
getJob(jobId).then(res => {
if (res.code === 200) {
queryParams.jobName = res.data.jobName;
queryParams.jobGroup = res.data.jobGroup;
fnGetList();
}
});
} else {
// 获取列表数据
fnGetList();
}
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="任务名称" name="jobName">
<a-input
v-model:value="queryParams.jobName"
:allow-clear="jobId === '0'"
:disabled="jobId !== '0'"
placeholder="请输入任务名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="任务组名" name="jobGroup">
<a-select
v-model:value="queryParams.jobGroup"
allow-clear
placeholder="请选择菜单状态"
:options="dict.sysJobGroup"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="执行状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择执行状态"
:options="dict.sysCommonStatus"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="记录时间" name="queryRangePicker">
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
value-format="YYYY-MM-DD"
:placeholder="['记录开始', '记录结束']"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button type="default" @click.prevent="fnClose()">
<template #icon><CloseOutlined /></template>
关闭
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['monitor:job:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
<a-button
type="dashed"
danger
@click.prevent="fnCleanList()"
v-perms:has="['monitor:job:remove']"
>
<template #icon><DeleteOutlined /></template>
清空
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['monitor:job:export']"
>
<template #icon><ExportOutlined /></template>
导出
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="jobLogId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:scroll="{ x: true }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'jobGroup'">
<DictTag :options="dict.sysJobGroup" :value="record.jobGroup" />
</template>
<template v-if="column.key === 'status'">
<a-tag :color="+record.status ? 'success' : 'error'">
{{ ['失败', '正常'][+record.status] }}
</a-tag>
</template>
<template v-if="column.key === 'jobLogId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record)"
v-perms:has="['monitor:job:query']"
>
<template #icon><ProfileOutlined /></template>
详情
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="日志编号" name="jobLogId">
{{ modalState.from.jobLogId }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="执行状态" name="status">
<a-tag :color="+modalState.from.status ? 'success' : 'error'">
{{ ['失败', '正常'][+modalState.from.status] }}
</a-tag>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="任务名称" name="jobName">
{{ modalState.from.jobName }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="任务组名" name="jobGroup">
<DictTag
:options="dict.sysJobGroup"
:value="modalState.from.jobGroup"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="调用目标" name="invokeTarget">
{{ modalState.from.invokeTarget }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="记录时间" name="createTime">
<span v-if="+modalState.from.createTime > 0">
{{ parseDateToStr(+modalState.from.createTime) }}
</span>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="传入参数" name="targetParams">
<a-textarea
v-model:value="modalState.from.targetParams"
:auto-size="{ minRows: 2, maxRows: 6 }"
placeholder="传入参数"
:disabled="true"
/>
</a-form-item>
<a-form-item label="日志信息" name="jobMsg">
<a-textarea
v-model:value="modalState.from.jobMsg"
:auto-size="{ minRows: 2, maxRows: 6 }"
placeholder="日志信息"
:disabled="true"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,546 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
exportLogininfor,
listLogininfor,
delLogininfor,
cleanLogininfor,
unlockLogininfor,
} from '@/api/monitor/logininfor';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
const { getDict } = useDictStore();
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**字典数据 */
let dict: {
/**登录状态 */
sysCommonStatus: DictType[];
} = reactive({
sysCommonStatus: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**登录地址 */
ipaddr: '',
/**登录账号 */
userName: '',
/**登录状态 */
status: undefined,
/**开始时间 */
beginTime: '',
/**结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
ipaddr: '',
userName: '',
status: undefined,
beginTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
/**勾选单个的登录账号 */
selectedUserName: string;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
selectedRowKeys: [],
selectedUserName: '',
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '日志编号',
dataIndex: 'infoId',
align: 'center',
},
{
title: '登录账号',
dataIndex: 'userName',
align: 'center',
},
{
title: '登录地址',
dataIndex: 'ipaddr',
align: 'center',
},
{
title: '登录地点',
dataIndex: 'loginLocation',
align: 'center',
},
{
title: '操作系统',
dataIndex: 'os',
align: 'center',
},
{
title: '浏览器',
dataIndex: 'browser',
align: 'center',
},
{
title: '登录状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '登录信息',
dataIndex: 'msg',
align: 'center',
},
{
title: '登录时间',
dataIndex: 'loginTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRows(
_: (string | number)[],
rows: Record<string, string>[]
) {
tableState.selectedRowKeys = rows.map(item => item.infoId);
// 针对单个登录账号解锁
if (rows.length === 1) {
tableState.selectedUserName = rows[0].userName;
} else {
tableState.selectedUserName = '';
}
}
/**记录删除 */
function fnRecordDelete() {
const ids = tableState.selectedRowKeys.join(',');
Modal.confirm({
title: '提示',
content: `确认删除访问编号为 【${ids}】 的数据项吗?`,
onOk() {
const hide = message.loading('请稍等...', 0);
delLogininfor(ids).then(res => {
hide();
if (res.code === 200) {
message.success({
content: `删除成功`,
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
fnGetList();
});
},
});
}
/**列表清空 */
function fnCleanList() {
Modal.confirm({
title: '提示',
content: `确认清空所有登录日志数据项?`,
onOk() {
const hide = message.loading('请稍等...', 0);
cleanLogininfor().then(res => {
hide();
if (res.code === 200) {
message.success({
content: `清空成功`,
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
fnGetList();
});
},
});
}
/**登录账号解锁 */
function fnUnlock() {
const username = tableState.selectedUserName;
Modal.confirm({
title: '提示',
content: `确认解锁用户 【${username}】 数据项?`,
onOk() {
const hide = message.loading('请稍等...', 0);
unlockLogininfor(username).then(res => {
hide();
if (res.code === 200) {
message.success({
content: `${username} 解锁成功`,
duration: 3,
});
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: '提示',
content: `确认根据搜索条件导出xlsx表格文件吗?`,
onOk() {
const hide = message.loading('正在打开...', 0);
exportLogininfor(toRaw(queryParams)).then(res => {
hide();
if (res.code === 200) {
message.success({
content: `已完成导出`,
duration: 2,
});
saveAs(res.data, `logininfor_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
});
},
});
}
/**查询登录日志列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listLogininfor(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_common_status')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysCommonStatus = resArr[0].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph>
对登录进行日志收集登录锁定的信息存入
<a-typography-text code>Redis</a-typography-text>
可对登录账号进行解锁
</a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="登录地址" name="ipaddr">
<a-input
v-model:value="queryParams.ipaddr"
allow-clear
:maxlength="128"
placeholder="请输入登录地址"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="登录账号" name="userName">
<a-input
v-model:value="queryParams.userName"
allow-clear
:maxlength="30"
placeholder="请输入登录账号"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="登录状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择登录状态"
:options="dict.sysCommonStatus"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="登录时间" name="queryRangePicker">
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
value-format="YYYY-MM-DD"
:placeholder="['登录开始', '登录结束']"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
:disabled="!tableState.selectedUserName"
@click.prevent="fnUnlock()"
v-perms:has="['monitor:logininfor:unlock']"
>
<template #icon><UnlockOutlined /></template>
解锁
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['monitor:logininfor:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
<a-button
type="dashed"
danger
@click.prevent="fnCleanList()"
v-perms:has="['monitor:logininfor:remove']"
>
<template #icon><DeleteOutlined /></template>
清空
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['monitor:logininfor:export']"
>
<template #icon><ExportOutlined /></template>
导出
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="infoId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:scroll="{ x: true }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRows,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysCommonStatus" :value="record.status" />
</template>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,338 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import { forceLogout, listOnline } from '@/api/monitor/online';
import { parseDateToStr } from '@/utils/date-utils';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**查询参数 */
let queryParams = reactive({
/**登录主机 */
ipaddr: '',
/**登录账号 */
userName: '',
});
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '序号',
dataIndex: 'num',
width: '50px',
align: 'center',
customRender(opt) {
const idxNum = (tablePagination.current - 1) * tablePagination.pageSize;
return idxNum + opt.index + 1;
},
},
{
title: '会话编号',
dataIndex: 'tokenId',
align: 'center',
},
{
title: '登录账号',
dataIndex: 'userName',
align: 'center',
},
{
title: '所属部门',
dataIndex: 'deptName',
align: 'center',
},
{
title: '登录主机',
dataIndex: 'ipaddr',
align: 'center',
},
{
title: '登录地点',
dataIndex: 'loginLocation',
align: 'center',
},
{
title: '操作系统',
dataIndex: 'os',
align: 'center',
},
{
title: '浏览器',
dataIndex: 'browser',
align: 'center',
},
{
title: '登录时间',
dataIndex: 'loginTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'tokenId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = {
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: true,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
},
};
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**查询参数重置 */
function fnQueryReset() {
queryParams.ipaddr = '';
queryParams.userName = '';
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/** 查询在线用户列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
listOnline(queryParams).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
tableState.data = res.rows;
}
tableState.loading = false;
});
}
/** 强退按钮操作 */
function fnForceLogout(row: Record<string, string>) {
Modal.confirm({
title: '提示',
content: `确认强退登录账号为 ${row.userName} 的用户?`,
onOk() {
const hide = message.loading('正在打开...', 0);
forceLogout(row.tokenId).finally(() => {
hide();
message.error({
content: `已强退用户 ${row.userName}`,
duration: 2,
});
});
fnGetList();
},
});
}
onMounted(() => {
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph>
登录用户
<a-typography-text code>Token</a-typography-text>
授权标识记录存储在
<a-typography-text code>Redis</a-typography-text>
可撤销对用户的授权拒绝用户请求并强制退出
</a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="登录账号" name="userName">
<a-input
v-model:value="queryParams.userName"
allow-clear
:maxlength="30"
placeholder="请输入登录账号"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="登录主机" name="ipaddr">
<a-input
v-model:value="queryParams.ipaddr"
allow-clear
:maxlength="128"
placeholder="请输入登录主机"
></a-input> </a-form-item
></a-col>
<a-col :lg="12" :md="24" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索
</a-button>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
{{ title }}
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="tokenId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:pagination="tablePagination"
:scroll="{ x: true }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'tokenId'">
<a-button
type="link"
@click.prevent="fnForceLogout(record)"
v-perms:has="['monitor:online:forceLogout']"
>
<template #icon><LogoutOutlined /></template>
强退
</a-button>
</template>
</template>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,692 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
exportOperlog,
listOperlog,
delOperlog,
cleanOperlog,
} from '@/api/monitor/operlog';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
const { getDict } = useDictStore();
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**字典数据 */
let dict: {
/**业务类型 */
sysBusinessType: DictType[];
/**登录状态 */
sysCommonStatus: DictType[];
} = reactive({
sysBusinessType: [],
sysCommonStatus: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**操作模块 */
title: '',
/**操作人员 */
operName: '',
/**业务类型 */
businessType: undefined,
/**操作状态 */
status: undefined,
/**开始时间 */
beginTime: '',
/**结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
title: '',
operName: '',
businessType: undefined,
status: undefined,
beginTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '日志编号',
dataIndex: 'operId',
align: 'center',
},
{
title: '模块名称',
dataIndex: 'title',
align: 'center',
},
{
title: '业务类型',
dataIndex: 'businessType',
key: 'businessType',
align: 'center',
},
{
title: '操作人员',
dataIndex: 'operName',
align: 'center',
},
{
title: '请求方式',
dataIndex: 'requestMethod',
align: 'center',
},
{
title: '请求主机',
dataIndex: 'operIp',
align: 'center',
},
{
title: '操作状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '操作日期',
dataIndex: 'operTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '消耗时间',
dataIndex: 'costTime',
key: 'costTime',
align: 'center',
customRender(opt) {
return `${opt.value} ms`;
},
},
{
title: '操作',
key: 'operId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
title: '操作日志',
from: {
operId: undefined,
businessType: 0,
deptName: '',
method: '',
operIp: '',
operLocation: '',
operMsg: '',
operName: '',
operParam: '',
operTime: 0,
operUrl: '',
operatorType: 1,
requestMethod: 'PUT',
status: 1,
title: '',
},
});
/**
* 对话框弹出显示为 查看
* @param row 操作日志信息对象
*/
function fnModalVisibleByVive(row: Record<string, string>) {
modalState.from = Object.assign(modalState.from, row);
modalState.title = '操作日志信息';
modalState.visibleByView = true;
}
/**
* 对话框弹出关闭执行函数
*/
function fnModalCancel() {
modalState.visibleByView = false;
}
/**记录删除 */
function fnRecordDelete() {
const ids = tableState.selectedRowKeys.join(',');
Modal.confirm({
title: '提示',
content: `确认删除访问编号为 【${ids}】 的数据项吗?`,
onOk() {
const key = 'delOperlog';
message.loading({ content: '请稍等...', key });
delOperlog(ids).then(res => {
if (res.code === 200) {
message.success({
content: '删除成功',
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**列表清空 */
function fnCleanList() {
Modal.confirm({
title: '提示',
content: `确认清空所有登录日志数据项?`,
onOk() {
const key = 'cleanOperlog';
message.loading({ content: '请稍等...', key });
cleanOperlog().then(res => {
if (res.code === 200) {
message.success({
content: '清空成功',
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: '提示',
content: `确认根据搜索条件导出xlsx表格文件吗?`,
onOk() {
const key = 'exportOperlog';
message.loading({ content: '请稍等...', key });
exportOperlog(toRaw(queryParams)).then(res => {
if (res.code === 200) {
message.success({
content: `已完成导出`,
key,
duration: 2,
});
saveAs(res.data, `operlog_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**查询登录日志列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listOperlog(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('sys_oper_type'),
getDict('sys_common_status'),
]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysBusinessType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.sysCommonStatus = resArr[1].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph>
对接口请求进行日志收集统计高频接口分析优化等操作
</a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="操作模块" name="title">
<a-input
v-model:value="queryParams.title"
allow-clear
placeholder="请输入操作模块"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="操作人员" name="operName">
<a-input
v-model:value="queryParams.operName"
allow-clear
placeholder="请输入操作人员"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="操作类型" name="businessType">
<a-select
v-model:value="queryParams.businessType"
allow-clear
placeholder="请选择操作类型"
:options="dict.sysBusinessType"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="操作状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择操作状态"
:options="dict.sysCommonStatus"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="操作时间" name="queryRangePicker">
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
value-format="YYYY-MM-DD"
:placeholder="['操作开始', '操作结束']"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['monitor:operlog:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
<a-button
type="dashed"
danger
@click.prevent="fnCleanList()"
v-perms:has="['monitor:operlog:remove']"
>
<template #icon><DeleteOutlined /></template>
清空
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['monitor:operlog:export']"
>
<template #icon><ExportOutlined /></template>
导出
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="operId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:scroll="{ x: true }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'businessType'">
<DictTag
:options="dict.sysBusinessType"
:value="record.businessType"
/>
</template>
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysCommonStatus" :value="record.status" />
</template>
<template v-if="column.key === 'operId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record)"
v-perms:has="['monitor:operlog:query']"
>
<template #icon><ProfileOutlined /></template>
详情
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="日志编号" name="operId">
{{ modalState.from.operId }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="执行状态" name="status">
<a-tag :color="+modalState.from.status ? 'success' : 'error'">
{{ ['失败', '正常'][+modalState.from.status] }}
</a-tag>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="业务类型" name="businessType">
{{ modalState.from.title }} /
<DictTag
:options="dict.sysBusinessType"
:value="modalState.from.businessType"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="操作人员" name="operName">
{{ modalState.from.operName }} / {{ modalState.from.operIp }} /
{{ modalState.from.operLocation }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="请求地址" name="operUrl">
{{ modalState.from.requestMethod }} -
{{ modalState.from.operUrl }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="操作时间" name="operTime">
<span v-if="+modalState.from.operTime > 0">
{{ parseDateToStr(+modalState.from.operTime) }}
</span>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="请求耗时" name="costTime">
{{ modalState.from.costTime }} ms
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="操作方法" name="method">
{{ modalState.from.method }}
</a-form-item>
</a-col>
</a-row>
<a-form-item label="请求参数" name="operParam">
<a-textarea
v-model:value="modalState.from.operParam"
:auto-size="{ minRows: 2, maxRows: 6 }"
placeholder="请求参数"
:disabled="true"
/>
</a-form-item>
<a-form-item label="操作信息" name="operMsg">
<a-textarea
v-model:value="modalState.from.operMsg"
:auto-size="{ minRows: 2, maxRows: 6 }"
placeholder="操作信息"
:disabled="true"
/>
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,329 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { getServer } from '@/api/monitor/server';
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**加载状态 */
let loading = ref<boolean>(true);
/**磁盘信息表格字段列 */
let diskTableColumns: ColumnsType = [
{
title: '路径盘符',
dataIndex: 'target',
align: 'center',
},
{
title: '总大小',
dataIndex: 'size',
align: 'center',
},
{
title: '剩余大小',
dataIndex: 'avail',
align: 'center',
},
{
title: '已使用大小',
dataIndex: 'used',
align: 'center',
},
{
title: '空间使用率(%)',
dataIndex: 'pcent',
align: 'center',
},
];
/**数据参数类型 */
type ServerType = {
/**CPU */
cpu: Record<string, string | number>;
/**磁盘 */
disk: Record<string, string>[];
/**内存 */
memory: Record<string, string | number>;
/**网络 */
network: Record<string, string>;
/**项目 */
project: Record<string, string>;
/**系统 */
system: Record<string, string | number>;
/**时间 */
time: Record<string, string | number>;
};
let server: ServerType = reactive({
cpu: {},
disk: [],
memory: {},
network: {},
project: {},
system: {},
time: {},
});
onMounted(() => {
getServer().then(res => {
if (res.code === 200 && res.data) {
// CPU信息
let cpu = res.data.cpu;
cpu.coreUsed = cpu.coreUsed.map((item: string) => item).join(' / ');
server.cpu = cpu;
// 磁盘信息
server.disk = res.data.disk;
// 内存信息
server.memory = res.data.memory;
// 网络信息
server.network = res.data.network;
// 项目信息
server.project = res.data.project;
// 系统信息
server.system = res.data.system;
// 时间信息
server.time = res.data.time;
// 加载状态
loading.value = false;
}
});
});
</script>
<template>
<PageContainer :title="title" :loading="loading">
<template #content>
<a-typography-paragraph> 服务器与应用程序的信息 </a-typography-paragraph>
</template>
<a-card
title="项目信息"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<a-descriptions
size="middle"
layout="horizontal"
:label-style="{ width: '140px' }"
:bordered="true"
:column="{ lg: 2, md: 2, xs: 1 }"
>
<a-descriptions-item label="项目名称">
{{ server.project.name }}
</a-descriptions-item>
<a-descriptions-item label="项目版本">
{{ server.project.version }}
</a-descriptions-item>
<a-descriptions-item label="项目环境">
{{ server.project.env }}
</a-descriptions-item>
<a-descriptions-item label="项目路径">
{{ server.project.appDir }}
</a-descriptions-item>
<a-descriptions-item label="项目依赖">
<a-tag
v-for="(value, name) in server.project.dependencies"
:key="name"
>
{{ name }}:{{ value }}
</a-tag>
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card
title="系统信息"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<a-descriptions
size="middle"
layout="horizontal"
:label-style="{ width: '140px' }"
:column="{ lg: 2, md: 2, xs: 1 }"
:bordered="true"
>
<a-descriptions-item
label="GO版本"
:span="2"
v-if="server.system && server.system.go"
>
{{ server.system.go }}
</a-descriptions-item>
<a-descriptions-item
label="Node版本"
v-if="server.system && server.system.node"
>
{{ server.system.node }}
</a-descriptions-item>
<a-descriptions-item
label="V8版本"
v-if="server.system && server.system.v8"
>
{{ server.system.v8 }}
</a-descriptions-item>
<a-descriptions-item label="进程PID号">
{{ server.system.processId }}
</a-descriptions-item>
<a-descriptions-item label="运行平台">
{{ server.system.platform }}
</a-descriptions-item>
<a-descriptions-item label="系统架构">
{{ server.system.arch }}
</a-descriptions-item>
<a-descriptions-item label="系统平台">
{{ server.system.uname }}
</a-descriptions-item>
<a-descriptions-item label="系统发行版本">
{{ server.system.release }}
</a-descriptions-item>
<a-descriptions-item label="主机名称">
{{ server.system.hostname }}
</a-descriptions-item>
<a-descriptions-item label="主机用户目录" :span="2">
{{ server.system.homeDir }}
</a-descriptions-item>
<a-descriptions-item label="项目路径" :span="2">
{{ server.system.cmd }}
</a-descriptions-item>
<a-descriptions-item label="执行命令" :span="2">
{{ server.system.execCommand }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card
title="CPU信息"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<a-descriptions
size="middle"
layout="horizontal"
:label-style="{ width: '140px' }"
:column="1"
:bordered="true"
>
<a-descriptions-item label="型号">
{{ server.cpu.model }}
</a-descriptions-item>
<a-descriptions-item label="速率Hz">
{{ server.cpu.speed }}
</a-descriptions-item>
<a-descriptions-item label="核心数">
{{ server.cpu.core }}
</a-descriptions-item>
<a-descriptions-item label="使用率(%)">
{{ server.cpu.coreUsed }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card
title="内存信息"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<a-descriptions
size="middle"
layout="horizontal"
:label-style="{ width: '140px' }"
:column="{ lg: 2, md: 2, xs: 1 }"
:bordered="true"
>
<a-descriptions-item label="总内存">
{{ server.memory.totalmem }}
</a-descriptions-item>
<a-descriptions-item label="剩余内存">
{{ server.memory.freemem }}
</a-descriptions-item>
<a-descriptions-item label="使用率(%)">
{{ server.memory.usage }}
</a-descriptions-item>
<a-descriptions-item label="进程总内存">
{{ server.memory.rss }}
</a-descriptions-item>
<a-descriptions-item label="堆的总大小">
{{ server.memory.heapTotal }}
</a-descriptions-item>
<a-descriptions-item label="堆已分配">
{{ server.memory.heapUsed }}
</a-descriptions-item>
<a-descriptions-item label="链接库占用">
{{ server.memory.external }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card
title="时间信息"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<a-descriptions
size="middle"
layout="horizontal"
:label-style="{ width: '140px' }"
:column="{ lg: 2, md: 2, xs: 1 }"
:bordered="true"
>
<a-descriptions-item label="时区">
{{ server.time.timezone }}
</a-descriptions-item>
<a-descriptions-item label="时间">
{{ server.time.current }}
</a-descriptions-item>
<a-descriptions-item label="时区名称">
{{ server.time.timezoneName }}
</a-descriptions-item>
<a-descriptions-item label="程序启动时间">
{{ server.time.uptime }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card
title="网络信息"
:bordered="false"
:body-style="{ marginBottom: '24px', padding: 0 }"
>
<a-descriptions
size="middle"
layout="horizontal"
:label-style="{ width: '140px' }"
:column="1"
:bordered="true"
>
<a-descriptions-item
:label="name"
v-for="(value, name) in server.network"
:key="name"
>
{{ value }}
</a-descriptions-item>
</a-descriptions>
</a-card>
<a-card title="磁盘信息" :bordered="false" :body-style="{ padding: 0 }">
<a-table
class="disk"
row-key="target"
size="middle"
:columns="diskTableColumns"
:data-source="server.disk"
:pagination="false"
:scroll="{ x: true }"
>
</a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
const router = useRouter();
const { params, query } = router.currentRoute.value;
router.replace({ path: `/${params.path}`, query });
</script>
<template>
<span>稍等...</span>
</template>

313
src/views/register.vue Normal file
View File

@@ -0,0 +1,313 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue/lib';
import { reactive, onMounted, toRaw } from 'vue';
import { getCaptchaImage, register } from '@/api/login';
import { regExpPasswd, regExpUserName } from '@/utils/regular-utils';
import { useRouter } from 'vue-router';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
const router = useRouter();
const codeImgFall =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
let state = reactive({
/**表单属性 */
form: {
/**账号 */
username: '',
/**密码 */
password: '',
/**确认密码 */
confirmPassword: '',
/**验证码 */
code: '',
/**验证码uuid */
uuid: '',
},
/**表单提交点击状态 */
formClick: false,
/**验证码状态 */
captcha: {
/**验证码开关 */
enabled: false,
/**验证码图片地址 */
codeImg: '',
codeImgFall: codeImgFall,
},
/**验证码点击状态 */
captchaClick: false,
});
/**表单验证确认密码是否一致 */
function fnEqualToPassword(
rule: Record<string, any>,
value: string,
callback: (error?: string) => void
) {
if (!value) {
return Promise.reject(t('views.register.passwordErr'));
}
if (state.form.password === value) {
return Promise.resolve();
}
return Promise.reject(t('views.register.passwordConfirmErr'));
}
/**表单验证通过 */
function fnFinish() {
state.formClick = true;
// 发送请求
const hide = message.loading(t('common.loading'), 0);
register(toRaw(state.form))
.then(res => {
if (res.code === 200) {
Modal.success({
title: t('common.tipTitle'),
content: t('views.register.tipContent', {
username: state.form.username,
}),
okText: t('views.register.tipBtn'),
onOk() {
router.push({ name: 'Login' });
},
});
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
state.formClick = false;
// 刷新验证码
if (state.captcha.enabled) {
state.form.code = '';
fnGetCaptcha();
}
});
}
/**
* 获取验证码
*/
function fnGetCaptcha() {
if (state.captchaClick) return;
state.captchaClick = true;
getCaptchaImage().then(res => {
state.captchaClick = false;
if (res.code != 200) {
message.warning(`${res.msg}`, 3);
return;
}
state.captcha.enabled = Boolean(res.captchaEnabled);
if (state.captcha.enabled) {
state.captcha.codeImg = res.img;
state.form.uuid = res.uuid;
}
});
}
onMounted(() => {
fnGetCaptcha();
});
</script>
<template>
<div class="container">
<div class="top">
<div class="header">
<a href="/" target="_self"
><img src="@/assets/logo.png" class="logo" alt="logo" />
<span class="title">{{ t('common.title') }}</span>
</a>
</div>
<div class="desc">{{ t('common.desc') }}</div>
</div>
<div class="main">
<a-form :model="state.form" name="tabForm" @finish="fnFinish">
<a-form-item
name="username"
:rules="[
{
required: true,
pattern: regExpUserName,
message: t('valid.userNameReg'),
},
]"
>
<a-input
v-model:value="state.form.username"
size="large"
:placeholder="t('valid.userNameHit')"
:maxlength="30"
>
<template #prefix>
<UserOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[
{
required: true,
pattern: regExpPasswd,
message: t('valid.passwordReg'),
},
]"
>
<a-input-password
v-model:value="state.form.password"
size="large"
:placeholder="t('valid.passwordHit')"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item
name="confirmPassword"
:rules="[
{
required: true,
min: 6,
max: 26,
validator: fnEqualToPassword,
},
]"
>
<a-input-password
v-model:value="state.form.confirmPassword"
size="large"
:placeholder="t('valid.passwordConfirmHit')"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-row :gutter="8" v-if="state.captcha.enabled">
<a-col :span="16">
<a-form-item
name="code"
:rules="[
{ required: true, min: 1, message: t('valid.codePlease') },
]"
>
<a-input
v-model:value="state.form.code"
size="large"
:placeholder="t('valid.codeHit')"
:maxlength="6"
>
<template #prefix>
<RobotOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="8">
<a-image
:alt="t('valid.codeHit')"
style="cursor: pointer; border-radius: 2px"
width="120px"
height="40px"
:preview="false"
:src="state.captcha.codeImg"
:fallback="state.captcha.codeImgFall"
@click="fnGetCaptcha"
/>
</a-col>
</a-row>
<a-button
block
type="primary"
size="large"
html-type="submit"
:loading="state.formClick"
>
{{ t('views.register.registerBtn') }}
</a-button>
<a-button
block
type="default"
size="large"
style="margin-top: 16px"
@click="() => router.push({ name: 'Login' })"
>
{{ t('views.register.loginBtn') }}
</a-button>
</a-form>
</div>
</div>
</template>
<style lang="less" scoped>
.container {
position: relative;
width: 100%;
min-height: 100%;
padding: 110px 0 144px;
background-image: url(../assets/background.svg);
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.top {
text-align: center;
a {
text-decoration: none;
}
.header {
height: 44px;
line-height: 44px;
.logo {
height: 44px;
margin-right: 16px;
vertical-align: top;
border-style: none;
border-radius: 6.66px;
}
.title {
position: relative;
top: 2px;
color: rgba(0, 0, 0, 0.85);
font-weight: 600;
font-size: 33px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
}
}
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
.main {
width: 368px;
min-width: 260px;
margin: 0 auto;
.prefix-icon {
color: #8c8c8c;
font-size: 16px;
}
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
}
</style>

View File

@@ -0,0 +1,809 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal, Form } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
exportConfig,
listConfig,
getConfig,
delConfig,
addConfig,
updateConfig,
refreshCache,
} from '@/api/system/config';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
const { getDict } = useDictStore();
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**字典数据 */
let dict: {
/**系统内置 */
sysYesNo: DictType[];
} = reactive({
sysYesNo: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**参数名称 */
configName: '',
/**参数键名 */
configKey: '',
/**系统内置 */
configType: undefined,
/**记录开始时间 */
beginTime: '',
/**记录结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
configName: '',
configKey: '',
configType: undefined,
beginTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '参数编号',
dataIndex: 'configId',
align: 'center',
},
{
title: '参数名称',
dataIndex: 'configName',
align: 'center',
},
{
title: '参数键名',
dataIndex: 'configKey',
align: 'center',
},
{
title: '参数键值',
dataIndex: 'configValue',
align: 'center',
},
{
title: '系统内置',
dataIndex: 'configType',
key: 'configType',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'configId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
title: '参数配置',
from: {
configId: undefined,
configName: '',
configKey: '',
configValue: '',
configType: 'N',
remark: undefined,
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
configName: [
{ required: true, min: 1, max: 50, message: '请正确输入参数名称' },
],
configKey: [
{ required: true, min: 1, max: 50, message: '请正确输入参数键名' },
],
configValue: [
{ required: true, min: 1, max: 50, message: '请正确输入参数键值' },
],
})
);
/**
* 对话框弹出显示为 查看
* @param configId 参数编号id
*/
function fnModalVisibleByVive(configId: string | number) {
if (!configId) {
message.error(`参数配置记录存在错误`, 2);
return;
}
getConfig(configId).then(res => {
if (res.code === 200 && res.data) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '参数配置信息';
modalState.visibleByView = true;
} else {
message.error(`获取参数配置信息失败`, 2);
}
});
}
/**
* 对话框弹出显示为 新增或者修改
* @param configId 参数编号id, 不传为新增
*/
function fnModalVisibleByEdit(configId?: string | number) {
if (!configId) {
modalStateFrom.resetFields();
modalState.title = '添加参数配置';
modalState.visibleByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getConfig(configId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200 && res.data) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '修改参数配置';
modalState.visibleByEdit = true;
} else {
message.error(`获取参数配置信息失败`, 2);
}
});
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
const config = from.configId ? updateConfig(from) : addConfig(from);
const key = 'config';
message.loading({ content: '请稍等...', key });
config
.then(res => {
if (res.code === 200) {
message.success({
content: `${modalState.title}成功`,
key,
duration: 2,
});
modalState.visibleByEdit = false;
modalStateFrom.resetFields();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
})
.finally(() => {
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(`请正确填写 ${e.errorFields.length} 处必填信息!`, 2);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.visibleByView = false;
modalStateFrom.resetFields();
}
/**
* 参数配置删除
* @param configId 参数编号ID
*/
function fnRecordDelete(configId: string = '0') {
if (configId === '0') {
configId = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: '提示',
content: `确认删除参数编号为 【${configId}】 的数据项?`,
onOk() {
const key = 'delConfig';
message.loading({ content: '请稍等...', key });
delConfig(configId).then(res => {
if (res.code === 200) {
message.success({
content: `删除成功`,
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key: key,
duration: 2,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: '提示',
content: `确认根据搜索条件导出xlsx表格文件吗?`,
onOk() {
const key = 'exportConfig';
message.loading({ content: '请稍等...', key });
exportConfig(toRaw(queryParams)).then(res => {
if (res.code === 200) {
message.success({
content: `已完成导出`,
key,
duration: 2,
});
saveAs(res.data, `config_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**
* 刷新缓存
*/
function fnRefreshCache() {
Modal.confirm({
title: '提示',
content: `确定要刷新参数配置缓存吗?`,
onOk() {
const key = 'refreshCache';
message.loading({ content: '请稍等...', key });
refreshCache().then(res => {
if (res.code === 200) {
message.success({
content: `刷新缓存成功`,
key,
duration: 2,
});
} else {
message.error({
content: `${res.msg}`,
key: key,
duration: 2,
});
}
});
},
});
}
/**查询参数配置列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listConfig(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_yes_no')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysYesNo = resArr[0].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph>
系统内可配置的参数变量
</a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="参数名称" name="configName">
<a-input
v-model:value="queryParams.configName"
allow-clear
placeholder="请输入参数名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="参数键名" name="configKey">
<a-input
v-model:value="queryParams.configKey"
allow-clear
placeholder="请输入参数键名"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item label="系统内置" name="configType">
<a-select
v-model:value="queryParams.configType"
allow-clear
placeholder="请选择"
:options="dict.sysYesNo"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item label="创建时间" name="queryRangePicker">
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
value-format="YYYY-MM-DD"
:placeholder="['创建开始', '创建结束']"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
@click.prevent="fnModalVisibleByEdit()"
v-perms:has="['system:config:add']"
>
<template #icon><PlusOutlined /></template>
新建
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['system:config:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
<a-button
type="dashed"
danger
@click.prevent="fnRefreshCache"
v-perms:has="['system:config:remove']"
>
<template #icon><SyncOutlined /></template>
刷新缓存
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['system:config:export']"
>
<template #icon><ExportOutlined /></template>
导出
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="configId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:pagination="tablePagination"
:scroll="{ x: true }"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'configType'">
<DictTag :options="dict.sysYesNo" :value="record.configType" />
</template>
<template v-if="column.key === 'configId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record.configId)"
v-perms:has="['system:config:query']"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>编辑</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.configId)"
v-perms:has="['system:config:edit']"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>删除</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.configId)"
v-perms:has="['system:config:remove']"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="参数名称" name="configName">
{{ modalState.from.configName }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="系统内置" name="configType">
<DictTag
:options="dict.sysYesNo"
:value="modalState.from.configType"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="参数键名" name="configKey">
{{ modalState.from.configKey }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="参数键值" name="configValue">
{{ modalState.from.configValue }}
</a-form-item>
</a-col>
</a-row>
<a-form-item label="参数说明" name="remark">
{{ modalState.from.remark }}
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
<!-- 新增框或修改框 -->
<a-modal
width="800px"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="参数名称"
name="configName"
v-bind="modalStateFrom.validateInfos.configName"
>
<a-input
v-model:value="modalState.from.configName"
allow-clear
placeholder="请输入参数名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="系统内置" name="configType">
<a-select
v-model:value="modalState.from.configType"
default-value="N"
placeholder="系统内置"
:options="dict.sysYesNo"
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="参数键名"
name="configKey"
v-bind="modalStateFrom.validateInfos.configKey"
>
<a-input
v-model:value="modalState.from.configKey"
allow-clear
placeholder="请输入参数名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="参数键值"
name="configValue"
v-bind="modalStateFrom.validateInfos.configValue"
>
<a-input
v-model:value="modalState.from.configValue"
allow-clear
placeholder="请输入参数键值"
></a-input>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="参数说明" name="remark">
<a-textarea
v-model:value="modalState.from.remark"
:auto-size="{ minRows: 4, maxRows: 6 }"
:maxlength="450"
:show-count="true"
placeholder="请输入参数说明"
/>
</a-form-item>
</a-form>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,800 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal, Form } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
listDept,
getDept,
delDept,
addDept,
updateDept,
listDeptExcludeChild,
} from '@/api/system/dept';
import { parseDateToStr } from '@/utils/date-utils';
import { regExpMobile, regExpEmail } from '@/utils/regular-utils';
import useDictStore from '@/store/modules/dict';
import { parseDataToTree } from '@/utils/parse-tree-utils';
const { getDict } = useDictStore();
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**字典数据 */
let dict: {
/**状态 */
sysNormalDisable: DictType[];
} = reactive({
sysNormalDisable: [],
});
/**查询参数 */
let queryParams = reactive({
/**部门名称 */
deptName: '',
/**部门状态 */
status: undefined,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
deptName: '',
status: undefined,
});
fnGetList();
}
/**表格全展开行key */
let expandedRowKeys: string[] = [];
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**全展开 */
expandedRowAll: boolean;
/**展开行key */
expandedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
expandedRowAll: false,
expandedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '部门名称',
dataIndex: 'deptName',
align: 'center',
},
{
title: '部门编号',
dataIndex: 'deptId',
align: 'center',
},
{
title: '部门排序',
dataIndex: 'orderNum',
align: 'center',
},
{
title: '岗位状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'deptId',
align: 'center',
},
];
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格展开行key */
function fnTableExpandedRowsAll(checked: boolean | string | number) {
tableState.expandedRowKeys = checked ? expandedRowKeys : [];
}
/**表格展开行key */
function fnTableExpandedRowsChange(expandedRows: (string | number)[]) {
tableState.expandedRowKeys = expandedRows;
}
/**初始上级部门选择树 */
let treeDataAll: Record<string, any>[] = [];
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
/**上级部门选择树 */
treeData: Record<string, any>[];
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
title: '部门',
from: {
deptId: undefined,
deptName: '',
email: '',
leader: '',
orderNum: 0,
parentId: '100',
ancestors: '',
parentName: null,
phone: '',
status: '0',
},
confirmLoading: false,
treeData: [],
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
parentId: [{ required: true, message: '上级部门不能为空' }],
deptName: [
{ required: true, min: 1, max: 30, message: '请正确输入部门名称' },
],
email: [
{
required: false,
pattern: regExpEmail,
message: '请输入正确的邮箱地址',
},
],
phone: [
{
required: false,
pattern: regExpMobile,
message: '请输入正确的手机号码',
},
],
})
);
/**
* 对话框弹出显示为 查看
* @param deptId 部门编号id
*/
function fnModalVisibleByVive(deptId: string | number) {
if (!deptId) {
message.error(`部门记录存在错误`, 2);
return;
}
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getDept(deptId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200 && res.data) {
if (res.data.parentId === '0') {
modalState.treeData = [
{ deptId: '0', parentId: '0', deptName: '根节点' },
];
} else {
modalState.treeData = treeDataAll;
}
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '部门信息';
modalState.visibleByView = true;
} else {
message.error(`获取部门信息失败`, 2);
}
});
}
/**
* 对话框弹出显示为 新增或者修改
* @param deptId 部门编号id, 不传为新增
* @param parentId 上级部门id
*/
function fnModalVisibleByEdit(
deptId?: string | number,
parentId?: string | number
) {
if (!deptId) {
modalStateFrom.resetFields();
if (parentId) {
modalState.from.parentId = parentId;
}
modalState.treeData = treeDataAll;
modalState.title = '添加部门信息';
modalState.visibleByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
// 获取部门信息同时查询部门列表(排除节点)
Promise.all([getDept(deptId), listDeptExcludeChild(deptId)])
.then(resArr => {
if (resArr[0].code === 200 && resArr[0].data) {
modalState.from = Object.assign(modalState.from, resArr[0].data);
if (resArr[1].code === 200 && Array.isArray(resArr[1].data)) {
if (resArr[1].data.length === 0) {
modalState.treeData = [
{ deptId: '0', parentId: '0', deptName: '根节点' },
];
} else {
modalState.treeData = parseDataToTree(resArr[1].data, 'deptId');
}
}
modalState.title = '修改部门信息';
modalState.visibleByEdit = true;
} else {
message.error(`获取部门信息失败`, 2);
}
})
.finally(() => {
modalState.confirmLoading = false;
hide();
});
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
const dept = from.deptId ? updateDept(from) : addDept(from);
const hide = message.loading('请稍等...', 0);
dept
.then(res => {
if (res.code === 200) {
message.success({
content: `${modalState.title}成功`,
duration: 2,
});
modalState.visibleByEdit = false;
// 新增时清空上级部门树重新获取
if (!from.deptId) {
treeDataAll = [];
}
modalStateFrom.resetFields();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(`请正确填写 ${e.errorFields.length} 处必填信息!`, 2);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.visibleByView = false;
modalStateFrom.resetFields();
}
/**
* 部门删除
* @param deptId 部门编号id
*/
function fnRecordDelete(deptId: string | number) {
Modal.confirm({
title: '提示',
content: `确认删除部门编号为 【${deptId}】 的数据项?`,
onOk() {
const hide = message.loading('请稍等...', 0);
delDept(deptId).then(res => {
hide();
if (res.code === 200) {
message.success({
content: `删除成功`,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
});
},
});
}
/**查询部门列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
listDept(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.data)) {
const treeData = parseDataToTree(res.data, 'deptId');
// 初始上级部门和展开编号key
if (treeDataAll.length <= 0) {
// 转换树状数据
treeDataAll = treeData;
// 展开编号key
expandedRowKeys = [...new Set(res.data.map(item => item.parentId))];
fnTableExpandedRowsAll(tableState.expandedRowAll);
}
tableState.data = treeData;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_normal_disable')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysNormalDisable = resArr[0].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph> 给予用户部门标记 </a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="部门名称" name="deptName">
<a-input
v-model:value="queryParams.deptName"
allow-clear
placeholder="请输入部门名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="岗位状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
@click.prevent="fnModalVisibleByEdit()"
v-perms:has="['system:dept:add']"
>
<template #icon><PlusOutlined /></template>
新建
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>展开/折叠</template>
<a-switch
v-model:checked="tableState.expandedRowAll"
checked-children=""
un-checked-children=""
size="small"
@change="fnTableExpandedRowsAll"
/>
</a-tooltip>
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="deptId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:pagination="false"
:scroll="{ x: true }"
children-column-name="children"
:expanded-row-keys="tableState.expandedRowKeys"
@expandedRowsChange="fnTableExpandedRowsChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysNormalDisable" :value="record.status" />
</template>
<template v-if="column.key === 'deptId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record.deptId)"
v-perms:has="['system:dept:query']"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>编辑</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.deptId)"
v-perms:has="['system:dept:edit']"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="record.parentId !== '0'">
<template #title>删除</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.deptId)"
v-perms:has="['system:dept:remove']"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip v-if="record.status !== '0'">
<template #title>新增子部门</template>
<a-button
type="link"
@click.prevent="
fnModalVisibleByEdit(undefined, record.deptId)
"
v-perms:has="['system:dept:add']"
>
<template #icon><PlusOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="上级部门" name="parentId">
<a-tree-select
:value="modalState.from.parentId"
placeholder="上级部门"
disabled
:tree-data="modalState.treeData"
:field-names="{
children: 'children',
label: 'deptName',
value: 'deptId',
}"
tree-node-label-prop="deptName"
>
<template #suffixIcon></template>
</a-tree-select>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="显示排序" name="orderNum">
{{ modalState.from.orderNum }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="部门状态" name="status">
<DictTag
:options="dict.sysNormalDisable"
:value="modalState.from.status"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="部门编号" name="deptId">
{{ modalState.from.deptId }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="部门名称" name="deptName">
{{ modalState.from.deptName }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="负责人" name="leader">
{{ modalState.from.leader }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="联系电话" name="phone">
{{ modalState.from.phone }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="邮箱" name="email">
{{ modalState.from.email }}
</a-form-item>
</a-col>
</a-row>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
<!-- 新增框或修改框 -->
<a-modal
width="800px"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-form-item
label="上级部门"
name="parentId"
v-bind="modalStateFrom.validateInfos.parentId"
>
<a-tree-select
v-model:value="modalState.from.parentId"
placeholder="上级部门"
show-search
tree-default-expand-all
:tree-data="modalState.treeData"
:field-names="{
children: 'children',
label: 'deptName',
value: 'deptId',
}"
tree-node-label-prop="deptName"
tree-node-filter-prop="deptName"
style="width: 100%"
:dropdown-style="{ maxHeight: '400px', overflow: 'auto' }"
>
</a-tree-select>
</a-form-item>
<a-form-item
label="部门名称"
name="deptName"
v-bind="modalStateFrom.validateInfos.deptName"
>
<a-input
v-model:value="modalState.from.deptName"
allow-clear
:maxlength="30"
placeholder="请输入部门名称"
></a-input>
</a-form-item>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="负责人"
name="leader"
v-bind="modalStateFrom.validateInfos.leader"
>
<a-input
v-model:value="modalState.from.leader"
allow-clear
placeholder="请输入负责人名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="24">
<a-form-item label="岗位状态" name="status">
<a-select
v-model:value="modalState.from.status"
default-value="0"
placeholder="岗位状态"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="24">
<a-form-item label="显示顺序" name="orderNum">
<a-input-number
v-model:value="modalState.from.orderNum"
:min="0"
:max="9999"
:step="1"
placeholder="排序值"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="联系电话"
name="phone"
v-bind="modalStateFrom.validateInfos.phone"
>
<a-input
v-model:value="modalState.from.phone"
allow-clear
:maxlength="11"
placeholder="请输入负责人联系电话"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="邮箱"
name="email"
v-bind="modalStateFrom.validateInfos.email"
>
<a-input
v-model:value="modalState.from.email"
allow-clear
:maxlength="40"
placeholder="请输入负责人邮箱"
></a-input>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
</style>

View File

@@ -0,0 +1,883 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { Form, message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
exportData,
listData,
getData,
delData,
addData,
updateData,
} from '@/api/system/dict/data';
import { getDictOptionselect, getType } from '@/api/system/dict/type';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useTabsStore from '@/store/modules/tabs';
import useDictStore from '@/store/modules/dict';
const tabsStore = useTabsStore();
const { parseDataDict, getDict } = useDictStore();
const route = useRoute();
const router = useRouter();
// 获取地址栏参数
const dictId = route.params && (route.params.dictId as string);
/**标签类型数据固定项 */
const tagTypeOptions = ref([
{ value: '', label: '普通文本' },
{ value: 'default', label: '默认default' },
{ value: 'blue ', label: '蓝色blue' },
{ value: 'cyan', label: '青色cyan' },
{ value: 'gold', label: '金色gold' },
{ value: 'green', label: '绿色green' },
{ value: 'lime', label: '亮绿lime' },
{ value: 'magenta', label: '紫红magenta' },
{ value: 'orange', label: '橘黄orange' },
{ value: 'pink', label: '粉色pink' },
{ value: 'purple', label: '紫色purple' },
{ value: 'red', label: '红色red' },
{ value: 'yellow', label: '黄色yellow' },
{ value: 'geekblue', label: '深蓝geekblue' },
{ value: 'volcano', label: '棕色volcano' },
{ value: 'processing', label: '进行processing' },
{ value: 'warning', label: '警告warning' },
{ value: 'error', label: '错误error' },
{ value: 'success', label: '成功success' },
]);
/**字典数据 */
let dict: {
/**数据状态 */
sysNormalDisable: DictType[];
/**字典名称 */
sysDictType: DictType[];
} = reactive({
sysNormalDisable: [],
sysDictType: [],
});
/**查询参数 */
let queryParams = reactive({
/**字典名称 */
dictType: '',
/**数据标签 */
dictLabel: '',
/**数据状态 */
status: undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
if (dictId && dictId !== '0') {
queryParams = Object.assign(queryParams, {
dictLabel: '',
status: undefined,
pageNum: 1,
pageSize: 20,
});
} else {
queryParams = Object.assign(queryParams, {
dictType: '',
dictLabel: '',
status: undefined,
pageNum: 1,
pageSize: 20,
});
}
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '数据代码',
dataIndex: 'dictCode',
align: 'center',
},
{
title: '数据标签',
dataIndex: 'dictLabel',
align: 'center',
},
{
title: '数据键值',
dataIndex: 'dictValue',
align: 'center',
},
{
title: '数据排序',
dataIndex: 'dictSort',
align: 'center',
},
{
title: '数据状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'dictCode',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
title: '字典数据',
from: {
dictCode: undefined,
dictLabel: '',
dictSort: 0,
dictType: 'sys_oper_type',
dictValue: '',
tagClass: '',
tagType: '',
remark: '',
status: '0',
createTime: 0,
createBy: undefined,
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
dictLabel: [
{ required: true, min: 1, max: 50, message: '请正确输入数据标签' },
],
dictValue: [
{ required: true, min: 1, max: 50, message: '请正确输入数据键值' },
],
})
);
/**
* 对话框弹出显示为 查看
* @param row 调度日志信息对象
*/
function fnModalVisibleByVive(row: Record<string, string>) {
modalState.from = Object.assign(modalState.from, row);
modalState.title = '字典数据信息';
modalState.visibleByView = true;
}
/**
* 对话框弹出显示为 新增或者修改
* @param dictCode 数据编号id, 不传为新增
*/
function fnModalVisibleByEdit(dictCode?: string | number) {
if (!dictCode) {
modalStateFrom.resetFields();
modalState.from.dictType = queryParams.dictType;
modalState.title = '添加字典数据';
modalState.visibleByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getData(dictCode).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '修改字典数据';
modalState.visibleByEdit = true;
} else {
message.error(`获取字典数据信息失败`, 2);
}
});
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
const dictData = from.dictCode ? updateData(from) : addData(from);
const key = 'dictData';
message.loading({ content: '请稍等...', key });
dictData
.then(res => {
if (res.code === 200) {
message.success({
content: `${modalState.title}成功`,
key,
duration: 2,
});
modalState.visibleByEdit = false;
modalStateFrom.resetFields();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
})
.finally(() => {
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(`请正确填写 ${e.errorFields.length} 处必填信息!`, 2);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.visibleByView = false;
modalStateFrom.resetFields();
}
/**
* 字典删除
* @param dictCode 字典代码
*/
function fnRecordDelete(dictCode: string = '0') {
if (dictCode === '0') {
dictCode = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: '提示',
content: `确认删除字典数据代码为 【${dictCode}】 的数据项?`,
onOk() {
const key = 'delData';
message.loading({ content: '请稍等...', key });
delData(dictCode).then(res => {
if (res.code === 200) {
message.success({
content: `删除成功`,
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key: key,
duration: 2,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: '提示',
content: `确认根据搜索条件导出xlsx表格文件吗?`,
onOk() {
const key = 'exportData';
message.loading({ content: '请稍等...', key });
exportData(toRaw(queryParams)).then(res => {
if (res.code === 200) {
message.success({
content: `已完成导出`,
key,
duration: 2,
});
saveAs(res.data, `dict_data_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**关闭跳转 */
function fnClose() {
const to = tabsStore.tabClose(route.path);
if (to) {
router.push(to);
} else {
router.back();
}
}
/**查询字典数据列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
listData(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('sys_normal_disable'),
getDictOptionselect(),
]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysNormalDisable = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
const dicts = resArr[1].value;
if (dicts.code === 200) {
dict.sysDictType = dicts.data;
}
}
});
// 指定字典id列表数据
if (dictId && dictId !== '0') {
getType(dictId).then(res => {
if (res.code === 200 && res.data) {
queryParams.dictType = res.data.dictType;
fnGetList();
} else {
message.error(`获取字典类型信息失败`, 3);
}
});
} else {
// 获取列表数据
fnGetList();
}
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="字典名称" name="dictType">
<a-select
v-model:value="queryParams.dictType"
:allow-clear="dictId === '0'"
:disabled="dictId !== '0'"
placeholder="请选择字典名称"
:options="dict.sysDictType"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="数据标签" name="dictLabel">
<a-input
v-model:value="queryParams.dictLabel"
allow-clear
placeholder="请输入数据标签"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="数据状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择数据状态"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button type="default" @click.prevent="fnClose()">
<template #icon><CloseOutlined /></template>
关闭
</a-button>
<a-button
type="primary"
@click.prevent="fnModalVisibleByEdit()"
v-perms:has="['system:dict:add']"
>
<template #icon><PlusOutlined /></template>
新增
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['system:dict:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['system:dict:export']"
>
<template #icon><ExportOutlined /></template>
导出
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="dictCode"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:scroll="{ x: true }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysNormalDisable" :value="record.status" />
</template>
<template v-if="column.key === 'dictCode'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record)"
v-perms:has="['system:dict:query']"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>编辑</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.dictCode)"
v-perms:has="['system:dict:edit']"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>删除</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.dictCode)"
v-perms:has="['system:dict:remove']"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="字典名称" name="dictType">
{{
dict.sysDictType.find(
item => item.value === modalState.from.dictType
)?.label
}}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="创建时间" name="createTime">
<span v-if="+modalState.from.createTime > 0">
{{ parseDateToStr(+modalState.from.createTime) }}
</span>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="数据代码" name="dictCode">
{{ modalState.from.dictCode }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="数据状态" name="status">
<DictTag
:options="dict.sysNormalDisable"
:value="modalState.from.status"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="数据标签" name="dictLabel">
{{ modalState.from.dictLabel }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="数据键值" name="dictValue">
{{ modalState.from.dictValue }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="标签类型" name="tagType">
<DictTag
:options="tagTypeOptions"
:value="modalState.from.tagType"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="样式属性" name="tagClass">
{{ modalState.from.tagClass }}
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="回显预览" name="tagType">
<DictTag
:options="parseDataDict(modalState.from)"
:value="modalState.from.dictValue"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="数据排序" name="dictSort">
{{ modalState.from.dictSort }}
</a-form-item>
</a-col>
</a-row>
<a-form-item label="数据说明" name="remark">
{{ modalState.from.remark }}
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
<!-- 新增框或修改框 -->
<a-modal
width="800px"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="字典类型" name="dictType">
<a-select
v-model:value="modalState.from.dictType"
default-value="sys_oper_type"
placeholder="字典类型"
:options="dict.sysDictType"
:disabled="true"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="数据状态" name="status">
<a-select
v-model:value="modalState.from.status"
default-value="0"
placeholder="数据状态"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="数据标签"
name="dictLabel"
v-bind="modalStateFrom.validateInfos.dictLabel"
>
<a-input
v-model:value="modalState.from.dictLabel"
allow-clear
placeholder="请输入数据标签"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="数据键值"
name="dictValue"
v-bind="modalStateFrom.validateInfos.dictValue"
>
<a-input
v-model:value="modalState.from.dictValue"
allow-clear
placeholder="请输入数据键值"
></a-input>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="标签类型" name="tagType">
<a-select
v-model:value="modalState.from.tagType"
placeholder="标签类型"
:options="tagTypeOptions"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="数据排序" name="dictSort">
<a-input
v-model:value="modalState.from.dictSort"
allow-clear
placeholder="请输入数据排序"
></a-input>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="样式属性" name="tagClass">
<a-input
v-model:value="modalState.from.tagClass"
allow-clear
placeholder="请输入样式属性"
></a-input>
</a-form-item>
<a-form-item label="数据说明" name="remark">
<a-textarea
v-model:value="modalState.from.remark"
:auto-size="{ minRows: 4, maxRows: 6 }"
:maxlength="450"
:show-count="true"
placeholder="请输入数据说明"
/>
</a-form-item>
</a-form>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,809 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal, Form } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
exportType,
listType,
getType,
delType,
addType,
updateType,
refreshCache,
} from '@/api/system/dict/type';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
import { MENU_PATH_INLINE } from '@/constants/menu-constants';
const { getDict } = useDictStore();
const route = useRoute();
const router = useRouter();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**字典数据 */
let dict: {
/**字典状态 */
sysNormalDisable: DictType[];
} = reactive({
sysNormalDisable: [],
});
/**开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**字典名称 */
dictName: '',
/**字典类型 */
dictType: '',
/**字典状态 */
status: undefined,
/**记录开始时间 */
beginTime: '',
/**记录结束时间 */
endTime: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
dictName: '',
dictType: '',
status: undefined,
beginTime: '',
endTime: '',
pageNum: 1,
pageSize: 20,
});
queryRangePicker.value = ['', ''];
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '字典编号',
dataIndex: 'dictId',
align: 'center',
},
{
title: '字典名称',
dataIndex: 'dictName',
align: 'center',
},
{
title: '字典类型',
dataIndex: 'dictType',
align: 'center',
},
{
title: '字典状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'dictId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
title: '字典类型',
from: {
dictId: undefined,
dictName: '',
dictType: undefined,
status: '0',
remark: undefined,
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
dictName: [
{ required: true, min: 1, max: 50, message: '请正确输入字典名称' },
],
dictType: [
{ required: true, min: 1, max: 50, message: '请正确输入字典类型' },
],
})
);
/**
* 对话框弹出显示为 查看
* @param dictId 字典编号id
*/
function fnModalVisibleByVive(dictId: string | number) {
if (!dictId) {
message.error(`字典类型记录存在错误`, 2);
return;
}
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getType(dictId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200 && res.data) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '字典类型信息';
modalState.visibleByView = true;
} else {
message.error(`获取字典类型信息失败`, 2);
}
});
}
/**
* 对话框弹出显示为 新增或者修改
* @param dictId 字典编号id, 不传为新增
*/
function fnModalVisibleByEdit(dictId?: string | number) {
if (!dictId) {
modalStateFrom.resetFields();
modalState.title = '添加字典类型';
modalState.visibleByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getType(dictId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200 && res.data) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '修改字典类型';
modalState.visibleByEdit = true;
} else {
message.error(`获取字典类型信息失败`, 2);
}
});
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
const dictType = from.dictId ? updateType(from) : addType(from);
const key = 'dictType';
message.loading({ content: '请稍等...', key });
dictType
.then(res => {
if (res.code === 200) {
message.success({
content: `${modalState.title}成功`,
key,
duration: 2,
});
modalState.visibleByEdit = false;
modalStateFrom.resetFields();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
})
.finally(() => {
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(`请正确填写 ${e.errorFields.length} 处必填信息!`, 2);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.visibleByView = false;
modalStateFrom.resetFields();
}
/**
* 字典删除
* @param dictId 字典编号ID
*/
function fnRecordDelete(dictId: string = '0') {
if (dictId === '0') {
dictId = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: '提示',
content: `确认删除参数编号为 【${dictId}】 的数据项?`,
onOk() {
const key = 'delType';
message.loading({ content: '请稍等...', key });
delType(dictId).then(res => {
if (res.code === 200) {
message.success({
content: `删除成功`,
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key: key,
duration: 2,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: '提示',
content: `确认根据搜索条件导出xlsx表格文件吗?`,
onOk() {
const key = 'exportType';
message.loading({ content: '请稍等...', key });
exportType(toRaw(queryParams)).then(res => {
if (res.code === 200) {
message.success({
content: `已完成导出`,
key,
duration: 2,
});
saveAs(res.data, `dict_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**
* 刷新缓存
*/
function fnRefreshCache() {
Modal.confirm({
title: '提示',
content: `确定要刷新字典数据缓存吗?`,
onOk() {
const key = 'refreshCache';
message.loading({ content: '请稍等...', key });
refreshCache().then(res => {
if (res.code === 200) {
message.success({
content: `刷新缓存成功`,
key,
duration: 2,
});
} else {
message.error({
content: `${res.msg}`,
key: key,
duration: 2,
});
}
});
},
});
}
/**跳转字典数据页面 */
function fnDataView(dictId: string | number = '0') {
router.push(`/system/dict${MENU_PATH_INLINE}/data/${dictId}`);
}
/**查询参数配置列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listType(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_normal_disable')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysNormalDisable = resArr[0].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph>
数据字典类型数据名称对应的代码值映射数据
</a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="字典名称" name="dictName">
<a-input
v-model:value="queryParams.dictName"
allow-clear
placeholder="请输入字典名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="字典类型" name="dictType">
<a-input
v-model:value="queryParams.dictType"
allow-clear
placeholder="请输入字典类型"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item label="字典状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item label="创建时间" name="queryRangePicker">
<a-range-picker
v-model:value="queryRangePicker"
allow-clear
bordered
value-format="YYYY-MM-DD"
:placeholder="['创建开始', '创建结束']"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
@click.prevent="fnModalVisibleByEdit()"
v-perms:has="['system:dict:add']"
>
<template #icon><PlusOutlined /></template>
新建
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['system:dict:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
<a-button
type="default"
@click.prevent="fnDataView()"
v-perms:has="['system:dict:data']"
>
<template #icon><ContainerOutlined /></template>
字典数据
</a-button>
<a-button
type="dashed"
danger
@click.prevent="fnRefreshCache"
v-perms:has="['system:dict:remove']"
>
<template #icon><SyncOutlined /></template>
刷新缓存
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['system:dict:export']"
>
<template #icon><ExportOutlined /></template>
导出
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="dictId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:pagination="tablePagination"
:scroll="{ x: true }"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysNormalDisable" :value="record.status" />
</template>
<template v-if="column.key === 'dictId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record.dictId)"
v-perms:has="['system:dict:query']"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>编辑</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.dictId)"
v-perms:has="['system:dict:edit']"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>删除</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.dictId)"
v-perms:has="['system:dict:remove']"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>字典数据</template>
<a-button
type="link"
@click.prevent="fnDataView(record.dictId)"
v-perms:has="['system:dict:data']"
>
<template #icon><ContainerOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="字典编号" name="dictId">
{{ modalState.from.dictId }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="字典状态" name="status">
<DictTag
:options="dict.sysNormalDisable"
:value="modalState.from.status"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="字典名称" name="dictName">
{{ modalState.from.dictName }}
</a-form-item>
<a-form-item label="字典类型" name="dictType">
{{ modalState.from.dictType }}
</a-form-item>
<a-form-item label="字典说明" name="remark">
{{ modalState.from.remark }}
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
<!-- 新增框或修改框 -->
<a-modal
width="800px"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="18" :md="18" :xs="24">
<a-form-item
label="字典名称"
name="dictName"
v-bind="modalStateFrom.validateInfos.dictName"
>
<a-input
v-model:value="modalState.from.dictName"
allow-clear
placeholder="请输入字典名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="24">
<a-form-item label="字典状态" name="status">
<a-select
v-model:value="modalState.from.status"
default-value="0"
placeholder="字典状态"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="18" :md="18" :xs="24">
<a-form-item
label="字典类型"
name="dictType"
v-bind="modalStateFrom.validateInfos.dictType"
>
<a-input
v-model:value="modalState.from.dictType"
allow-clear
placeholder="请输入字典类型"
></a-input>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="字典说明" name="remark">
<a-textarea
v-model:value="modalState.from.remark"
:auto-size="{ minRows: 4, maxRows: 6 }"
:maxlength="450"
:show-count="true"
placeholder="请输入参数说明"
/>
</a-form-item>
</a-form>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,734 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal, Form } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
listNotice,
getNotice,
delNotice,
addNotice,
updateNotice,
} from '@/api/system/notice';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
const { getDict } = useDictStore();
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**字典数据 */
let dict: {
/**公告类型 */
sysNoticeType: DictType[];
/**公告状态 */
sysNoticeStatus: DictType[];
} = reactive({
sysNoticeType: [],
sysNoticeStatus: [],
});
/**查询参数 */
let queryParams = reactive({
/**公告标题 */
noticeTitle: '',
/**创建者 */
createBy: undefined,
/**公告类型 */
noticeType: undefined,
/**公告状态 */
status: undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
noticeTitle: '',
createBy: '',
noticeType: undefined,
status: undefined,
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '公告编号',
dataIndex: 'noticeId',
align: 'center',
},
{
title: '公告标题',
dataIndex: 'noticeTitle',
align: 'center',
},
{
title: '公告类型',
dataIndex: 'noticeType',
key: 'noticeType',
align: 'center',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建者',
dataIndex: 'createBy',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if(+opt.value <= 0) return ''
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'noticeId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
title: '公告',
from: {
noticeId: undefined,
noticeTitle: '',
noticeContent: '',
noticeType: '2',
status: '1',
delFlag: '0',
remark: '',
createBy: undefined,
createTime: undefined,
updateBy: undefined,
updateTime: undefined,
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
noticeTitle: [
{ required: true, min: 2, max: 50, message: '请正确输入公告标题' },
],
noticeType: [{ required: true, message: '请选择公告类型' }],
noticeContent: [
{
required: true,
min: 2,
max: 3000,
message: '请正确输入公告内容限10-3000个字符',
},
],
})
);
/**
* 对话框弹出显示为 查看
* @param noticeId 公告id
*/
function fnModalVisibleByVive(noticeId: string | number) {
if (!noticeId) {
message.error(`公告记录存在错误`, 2);
return;
}
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getNotice(noticeId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '公告信息';
modalState.visibleByView = true;
} else {
message.error(`获取公告信息失败`, 2);
}
});
}
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 公告id, 不传为新增
*/
function fnModalVisibleByEdit(noticeId?: string | number) {
if (!noticeId) {
modalStateFrom.resetFields();
modalState.title = '添加公告';
modalState.visibleByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getNotice(noticeId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '修改公告';
modalState.visibleByEdit = true;
} else {
message.error(`获取公告信息失败`, 2);
}
});
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
const notice = from.noticeId ? updateNotice(from) : addNotice(from);
const key = 'notice';
message.loading({ content: '请稍等...', key });
notice
.then(res => {
if (res.code === 200) {
message.success({
content: `${modalState.title}成功`,
key,
duration: 2,
});
modalState.visibleByEdit = false;
modalStateFrom.resetFields();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
})
.finally(() => {
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(`请正确填写 ${e.errorFields.length} 处必填信息!`, 2);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.visibleByView = false;
modalStateFrom.resetFields();
}
/**
* 公告删除
* @param noticeId 公告编号ID
*/
function fnRecordDelete(noticeId: string = '0') {
if (noticeId === '0') {
noticeId = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: '提示',
content: `确认删除公告编号为 【${noticeId}】 的数据项?`,
onOk() {
const key = 'delNotice';
message.loading({ content: '请稍等...', key });
delNotice(noticeId).then(res => {
if (res.code === 200) {
message.success({
content: `删除成功`,
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key: key,
duration: 2,
});
}
});
},
});
}
/**查询公告列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
listNotice(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([
getDict('sys_notice_type'),
getDict('sys_notice_status'),
]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysNoticeType = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.sysNoticeStatus = resArr[1].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph>
发布公告给內部用户的通知
</a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="公告标题" name="noticeTitle">
<a-input
v-model:value="queryParams.noticeTitle"
allow-clear
placeholder="请输入公告标题"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="创建者" name="createBy">
<a-input
v-model:value="queryParams.createBy"
allow-clear
placeholder="请输入创建者"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="公告类型" name="noticeType">
<a-select
v-model:value="queryParams.noticeType"
allow-clear
placeholder="请选择公告类型"
:options="dict.sysNoticeType"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="公告状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择公告状态"
:options="dict.sysNoticeStatus"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
@click.prevent="fnModalVisibleByEdit()"
v-perms:has="['system:notice:add']"
>
<template #icon><PlusOutlined /></template>
新建
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['system:notice:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="noticeId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:pagination="tablePagination"
:scroll="{ x: true }"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'noticeType'">
<DictTag :options="dict.sysNoticeType" :value="record.noticeType" />
</template>
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysNoticeStatus" :value="record.status" />
</template>
<template v-if="column.key === 'noticeId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record.noticeId)"
v-perms:has="['system:notice:query']"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>编辑</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.noticeId)"
v-perms:has="['system:notice:edit']"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>删除</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.noticeId)"
v-perms:has="['system:notice:remove']"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="公告标题" name="noticeTitle">
{{ modalState.from.noticeTitle }}
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="24">
<a-form-item label="公告类型" name="noticeType">
<DictTag
:options="dict.sysNoticeType"
:value="modalState.from.noticeType"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="24">
<a-form-item label="公告状态" name="status">
<DictTag
:options="dict.sysNoticeStatus"
:value="modalState.from.status"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="公告内容" name="noticeContent">
{{ modalState.from.noticeContent }}
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
<!-- 新增框或修改框 -->
<a-modal
width="800px"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="公告标题"
name="noticeTitle"
v-bind="modalStateFrom.validateInfos.noticeTitle"
>
<a-input
v-model:value="modalState.from.noticeTitle"
allow-clear
placeholder="请输入公告标题"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="24">
<a-form-item
label="公告类型"
name="noticeType"
v-bind="modalStateFrom.validateInfos.noticeType"
>
<a-select
v-model:value="modalState.from.noticeType"
default-value="1"
placeholder="公告类型"
:options="dict.sysNoticeType"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="6" :xs="24">
<a-form-item label="公告状态" name="status">
<a-select
v-model:value="modalState.from.status"
default-value="0"
placeholder="公告状态"
:options="dict.sysNoticeStatus"
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="公告内容"
name="noticeContent"
v-bind="modalStateFrom.validateInfos.noticeContent"
>
<a-textarea
v-model:value="modalState.from.noticeContent"
:auto-size="{ minRows: 4, maxRows: 14 }"
:maxlength="3000"
:show-count="true"
placeholder="请输入公告内容"
/>
</a-form-item>
</a-form>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,759 @@
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal, Form } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import {
exportPost,
listPost,
addPost,
delPost,
getPost,
updatePost,
} from '@/api/system/post';
import { saveAs } from 'file-saver';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
const { getDict } = useDictStore();
const route = useRoute();
/**路由标题 */
let title = ref<string>(route.meta.title ?? '标题');
/**字典数据 */
let dict: {
/**状态 */
sysNormalDisable: DictType[];
} = reactive({
sysNormalDisable: [],
});
/**查询参数 */
let queryParams = reactive({
/**岗位编码 */
postCode: '',
/**岗位名称 */
postName: '',
/**岗位状态 */
status: undefined,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
postCode: '',
postName: '',
status: undefined,
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '岗位编号',
dataIndex: 'postId',
align: 'center',
},
{
title: '岗位编码',
dataIndex: 'postCode',
align: 'center',
},
{
title: '岗位名称',
dataIndex: 'postName',
align: 'center',
},
{
title: '岗位排序',
dataIndex: 'postSort',
align: 'center',
},
{
title: '岗位状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'postId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
title: '岗位',
from: {
postId: undefined,
postName: '',
postCode: '',
postSort: 0,
status: '0',
remark: '',
createTime: 0,
},
confirmLoading: false,
});
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
postName: [
{ required: true, min: 1, max: 50, message: '请正确输入岗位编码' },
],
postCode: [
{ required: true, min: 1, max: 50, message: '请正确输入岗位名称' },
],
})
);
/**
* 对话框弹出显示为 查看
* @param postId 岗位编号id
*/
function fnModalVisibleByVive(postId: string | number) {
if (!postId) {
message.error(`岗位记录存在错误`, 2);
return;
}
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getPost(postId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200 && res.data) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '岗位信息';
modalState.visibleByView = true;
} else {
message.error(`获取岗位信息失败`, 2);
}
});
}
/**
* 对话框弹出显示为 新增或者修改
* @param postId 岗位编号id, 不传为新增
*/
function fnModalVisibleByEdit(postId?: string | number) {
if (!postId) {
modalStateFrom.resetFields();
modalState.title = '添加岗位信息';
modalState.visibleByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading('正在打开...', 0);
modalState.confirmLoading = true;
getPost(postId).then(res => {
modalState.confirmLoading = false;
hide();
if (res.code === 200 && res.data) {
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = '修改岗位信息';
modalState.visibleByEdit = true;
} else {
message.error(`获取岗位信息失败`, 2);
}
});
}
}
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(() => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
const post = from.postId ? updatePost(from) : addPost(from);
const key = 'notice';
message.loading({ content: '请稍等...', key });
post
.then(res => {
if (res.code === 200) {
message.success({
content: `${modalState.title}成功`,
key,
duration: 2,
});
modalState.visibleByEdit = false;
modalStateFrom.resetFields();
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
})
.finally(() => {
modalState.confirmLoading = false;
});
})
.catch(e => {
message.error(`请正确填写 ${e.errorFields.length} 处必填信息!`, 2);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.visibleByView = false;
modalStateFrom.resetFields();
}
/**
* 岗位删除
* @param postId 岗位编号ID
*/
function fnRecordDelete(postId: string = '0') {
if (postId === '0') {
postId = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: '提示',
content: `确认删除岗位编号为 【${postId}】 的数据项?`,
onOk() {
const key = 'delPost';
message.loading({ content: '请稍等...', key });
delPost(postId).then(res => {
if (res.code === 200) {
message.success({
content: `删除成功`,
key,
duration: 2,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
key: key,
duration: 2,
});
}
});
},
});
}
/**列表导出 */
function fnExportList() {
Modal.confirm({
title: '提示',
content: `确认根据搜索条件导出xlsx表格文件吗?`,
onOk() {
const key = 'exportPost';
message.loading({ content: '请稍等...', key });
exportPost(toRaw(queryParams)).then(res => {
if (res.code === 200) {
message.success({
content: `已完成导出`,
key,
duration: 2,
});
saveAs(res.data, `post_${Date.now()}.xlsx`);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
},
});
}
/**查询岗位列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
listPost(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_normal_disable')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysNormalDisable = resArr[0].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer :title="title">
<template #content>
<a-typography-paragraph> 给予用户岗位标记 </a-typography-paragraph>
</template>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="岗位编码" name="postCode">
<a-input
v-model:value="queryParams.postCode"
allow-clear
placeholder="请输入岗位编码"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="岗位名称" name="postName">
<a-input
v-model:value="queryParams.postName"
allow-clear
placeholder="请输入岗位名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="岗位状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
@click.prevent="fnModalVisibleByEdit()"
v-perms:has="['system:post:add']"
>
<template #icon><PlusOutlined /></template>
新建
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete()"
v-perms:has="['system:post:remove']"
>
<template #icon><DeleteOutlined /></template>
删除
</a-button>
<a-button
type="dashed"
@click.prevent="fnExportList()"
v-perms:has="['system:post:export']"
>
<template #icon><ExportOutlined /></template>
导出
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="postId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:pagination="tablePagination"
:scroll="{ x: true }"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysNormalDisable" :value="record.status" />
</template>
<template v-if="column.key === 'postId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>查看详情</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByVive(record.postId)"
v-perms:has="['system:post:query']"
>
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>编辑</template>
<a-button
type="link"
@click.prevent="fnModalVisibleByEdit(record.postId)"
v-perms:has="['system:post:edit']"
>
<template #icon><FormOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>删除</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.postId)"
v-perms:has="['system:post:remove']"
>
<template #icon><DeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 详情框 -->
<a-modal
width="800px"
:visible="modalState.visibleByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="岗位编号" name="postId">
{{ modalState.from.postId }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="创建时间" name="createTime">
<span v-if="+modalState.from.createTime > 0">
{{ parseDateToStr(+modalState.from.createTime) }}
</span>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="岗位顺序" name="postSort">
{{ modalState.from.postSort }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="岗位状态" name="status">
<DictTag
:options="dict.sysNormalDisable"
:value="modalState.from.status"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="岗位编码" name="postCode">
{{ modalState.from.postCode }}
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="岗位名称" name="postName">
{{ modalState.from.postName }}
</a-form-item>
</a-col>
</a-row>
<a-form-item label="岗位说明" name="remark">
{{ modalState.from.remark }}
</a-form-item>
</a-form>
<template #footer>
<a-button key="cancel" @click="fnModalCancel">关闭</a-button>
</template>
</a-modal>
<!-- 新增框或修改框 -->
<a-modal
width="800px"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="岗位编码"
name="postCode"
v-bind="modalStateFrom.validateInfos.postCode"
>
<a-input
v-model:value="modalState.from.postCode"
allow-clear
placeholder="请输入岗位编码"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="岗位状态" name="status">
<a-select
v-model:value="modalState.from.status"
default-value="0"
placeholder="岗位状态"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
label="岗位名称"
name="postName"
v-bind="modalStateFrom.validateInfos.postName"
>
<a-input
v-model:value="modalState.from.postName"
allow-clear
placeholder="请输入岗位名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item label="岗位顺序" name="postSort">
<a-input-number
v-model:value="modalState.from.postSort"
:min="0"
:max="9999"
:step="1"
placeholder="排序值"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="岗位说明" name="remark">
<a-textarea
v-model:value="modalState.from.remark"
:auto-size="{ minRows: 4, maxRows: 6 }"
:maxlength="450"
:show-count="true"
placeholder="请输入岗位说明"
/>
</a-form-item>
</a-form>
</a-modal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,508 @@
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { reactive, onMounted, toRaw } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import AuthUserSelect from './components/auth-user-select.vue';
import { authUserAllocatedList, authUserChecked } from '@/api/system/role';
import { parseDateToStr } from '@/utils/date-utils';
import useTabsStore from '@/store/modules/tabs';
import useDictStore from '@/store/modules/dict';
const tabsStore = useTabsStore();
const { getDict } = useDictStore();
const route = useRoute();
const router = useRouter();
// 获取地址栏参数
const roleId = route.params && (route.params.roleId as string);
const roleName = route.query && (route.query.roleName as string);
/**字典数据 */
let dict: {
/**状态 */
sysNormalDisable: DictType[];
} = reactive({
sysNormalDisable: [],
});
/**查询参数 */
let queryParams = reactive({
/**登录账号 */
userName: '',
/**手机号码 */
phonenumber: '',
/**用户状态 */
status: undefined,
/**角色ID */
roleId: roleId,
/**是否已分配 */
allocated: true,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
userName: '',
phonenumber: '',
status: undefined,
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**斑马纹 */
striped: boolean;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
striped: false,
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '用户编号',
dataIndex: 'userId',
align: 'center',
},
{
title: '登录账号',
dataIndex: 'userName',
align: 'center',
},
{
title: '用户昵称',
dataIndex: 'nickName',
align: 'center',
},
{
title: '手机号码',
dataIndex: 'phonenumber',
align: 'center',
},
{
title: '电子邮箱',
dataIndex: 'email',
align: 'center',
},
{
title: '用户状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
{
title: '操作',
key: 'userId',
align: 'center',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**表格斑马纹 */
function fnTableStriped(_record: unknown, index: number) {
return tableState.striped && index % 2 === 1 ? 'table-striped' : undefined;
}
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**选择用户框是否显示 */
visibleBySelectUser: boolean;
/**标题 */
title: string;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleBySelectUser: false,
title: '选择用户',
});
/**
* 对话框弹出显示为 选择用户
*/
function fnModalVisibleBySelectUser() {
modalState.visibleBySelectUser = true;
}
/**
* 对话框弹出确认执行函数
* 授权用户
*/
function fnModalOk(userIds: string[] | number[]) {
if (userIds.length <= 0) {
message.error(`请选择要分配的用户`, 2);
return;
}
const hide = message.loading('请稍等...', 0);
authUserChecked({
checked: true,
userIds: userIds.join(','),
roleId: roleId,
}).then(res => {
hide();
if (res.code === 200) {
modalState.visibleBySelectUser = false;
message.success({
content: `授权用户添加成功`,
duration: 3,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
});
}
/**
* 取消授权
* @param userId 用户编号ID
*/
function fnRecordDelete(userId: string | number) {
if (userId === '0') {
userId = tableState.selectedRowKeys.join(',');
}
Modal.confirm({
title: '提示',
content: `确认取消用户编号为 【${userId}】 的数据项授权?`,
onOk() {
const hide = message.loading('请稍等...', 0);
authUserChecked({ checked: false, userIds: userId, roleId: roleId }).then(
res => {
hide();
if (res.code === 200) {
message.success({
content: `取消授权成功`,
duration: 3,
});
fnGetList();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
}
);
},
});
}
/**关闭跳转 */
function fnClose() {
const to = tabsStore.tabClose(route.path);
if (to) {
router.push(to);
} else {
router.back();
}
}
/**查询角色已授权用户列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
authUserAllocatedList(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
onMounted(() => {
// 初始字典数据
Promise.allSettled([getDict('sys_normal_disable')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysNormalDisable = resArr[0].value;
}
});
// 获取列表数据
fnGetList();
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="角色名称" name="roleName">
<a-input
:value="roleName"
disabled
placeholder="请输入角色名称"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="登录账号" name="userName">
<a-input
v-model:value="queryParams.userName"
allow-clear
:maxlength="30"
placeholder="请输入登录账号"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="手机号码" name="phonenumber">
<a-input
v-model:value="queryParams.phonenumber"
allow-clear
:maxlength="11"
placeholder="请输入手机号码"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item label="用户状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择用户状态"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button type="default" @click.prevent="fnClose()">
<template #icon><CloseOutlined /></template>
关闭
</a-button>
<a-button
type="primary"
@click.prevent="fnModalVisibleBySelectUser()"
v-perms:has="['system:role:add']"
>
<template #icon><UsergroupAddOutlined /></template>
分配用户
</a-button>
<a-button
type="default"
danger
:disabled="tableState.selectedRowKeys.length <= 0"
@click.prevent="fnRecordDelete('0')"
v-perms:has="['system:role:remove']"
>
<template #icon><UsergroupDeleteOutlined /></template>
批量取消授权
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>搜索栏</template>
<a-switch
v-model:checked="tableState.seached"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>表格斑马纹</template>
<a-switch
v-model:checked="tableState.striped"
checked-children=""
un-checked-children=""
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>刷新</template>
<a-button type="text" @click.prevent="fnGetList">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>密度</template>
<a-dropdown trigger="click">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">默认</a-menu-item>
<a-menu-item key="middle">中等</a-menu-item>
<a-menu-item key="small">紧凑</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="userId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:row-class-name="fnTableStriped"
:scroll="{ x: true }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysNormalDisable" :value="record.status" />
</template>
<template v-if="column.key === 'userId'">
<a-space :size="8" align="center">
<a-tooltip>
<template #title>取消授权</template>
<a-button
type="link"
@click.prevent="fnRecordDelete(record.userId)"
v-perms:has="['system:role:remove']"
>
<template #icon><UserDeleteOutlined /></template>
</a-button>
</a-tooltip>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 分配用户选择框 -->
<AuthUserSelect
:role-id="roleId"
:title="modalState.title"
v-model:visible="modalState.visibleBySelectUser"
@ok="fnModalOk"
/>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.table-striped) td {
background-color: #fafafa;
}
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,301 @@
<script setup lang="ts">
import { reactive, toRaw, watch } from 'vue';
import { message } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { authUserAllocatedList } from '@/api/system/role';
import { parseDateToStr } from '@/utils/date-utils';
import useDictStore from '@/store/modules/dict';
const { getDict } = useDictStore();
const emit = defineEmits(['ok', 'cancel', 'update:visible']);
const props = defineProps({
title: {
type: String,
default: '标题',
},
visible: {
type: Boolean,
default: false,
},
roleId: {
type: [Number, String],
required: true,
},
});
/**字典数据 */
let dict: {
/**状态 */
sysNormalDisable: DictType[];
} = reactive({
sysNormalDisable: [],
});
/**查询参数 */
let queryParams = reactive({
/**登录账号 */
userName: '',
/**手机号码 */
phonenumber: '',
/**用户状态 */
status: undefined,
/**角色ID */
roleId: props.roleId,
/**是否已分配 */
allocated: false,
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
userName: '',
phonenumber: '',
status: undefined,
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'small',
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: '用户编号',
dataIndex: 'userId',
align: 'center',
},
{
title: '登录账号',
dataIndex: 'userName',
align: 'center',
},
{
title: '用户昵称',
dataIndex: 'nickName',
align: 'center',
},
{
title: '手机号码',
dataIndex: 'phonenumber',
align: 'center',
},
{
title: '用户状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (+opt.value <= 0) return '';
return parseDateToStr(+opt.value);
},
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => `总共 ${total}`,
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格多选 */
function fnTableSelectedRowKeys(keys: (string | number)[]) {
tableState.selectedRowKeys = keys;
}
/**查询角色未授权用户列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
authUserAllocatedList(toRaw(queryParams)).then(res => {
if (res.code === 200 && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
}
tableState.loading = false;
});
}
/**弹框确认按钮事件 */
function fnModalOk() {
const userIds = tableState.selectedRowKeys;
if (userIds.length <= 0) {
message.error(`请选择要分配的用户`, 2);
return;
}
emit('update:visible', false);
emit('ok', userIds);
}
/**弹框取消按钮事件 */
function fnModalCancel() {
emit('update:visible', false);
emit('cancel');
}
/**显示弹框时初始数据 */
function init() {
// 初始字典数据
Promise.allSettled([getDict('sys_normal_disable')]).then(resArr => {
if (resArr[0].status === 'fulfilled') {
dict.sysNormalDisable = resArr[0].value;
}
});
// 获取列表数据
fnGetList();
}
/**监听是否显示,初始数据 */
watch(
() => props.visible,
val => {
if (val) init();
}
);
</script>
<template>
<a-modal
width="800px"
:title="props.title"
:visible="props.visible"
:keyboard="false"
:mask-closable="false"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="8" :md="12" :xs="24">
<a-form-item label="登录账号" name="userName">
<a-input
v-model:value="queryParams.userName"
allow-clear
:maxlength="30"
placeholder="请输入登录账号"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item label="手机号码" name="phonenumber">
<a-input
v-model:value="queryParams.phonenumber"
allow-clear
:maxlength="11"
placeholder="请输入手机号码"
></a-input>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :xs="24">
<a-form-item label="用户状态" name="status">
<a-select
v-model:value="queryParams.status"
allow-clear
placeholder="请选择用户状态"
:options="dict.sysNormalDisable"
>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList">
<template #icon><SearchOutlined /></template>
搜索</a-button
>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
重置</a-button
>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<a-table
class="table"
row-key="userId"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:scroll="{ scrollToFirstRowOnChange: true, y: 400, x: true }"
:pagination="tablePagination"
:row-selection="{
type: 'checkbox',
onChange: fnTableSelectedRowKeys,
}"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<DictTag :options="dict.sysNormalDisable" :value="record.status" />
</template>
</template>
</a-table>
</a-modal>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,185 @@
<script setup lang="ts">
import { reactive } from 'vue';
import { message } from 'ant-design-vue/lib';
import { FileType } from 'ant-design-vue/lib/upload/interface';
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import { ResultType } from '@/plugins/http-fetch';
const emit = defineEmits(['close', 'update:visible']);
const props = defineProps({
/**窗口标题 */
title: {
type: String,
default: '标题',
},
/**是否弹出显示,必传 */
visible: {
type: Boolean,
required: true,
},
/**文件上传函数方法,必传 */
uploadFileMethod: {
type: Function,
required: true,
},
/**下载模板函数方法 */
downloadTemplateMethod: {
type: Function,
default: undefined,
},
/**显示更新已存在数据勾选项 */
showUpdateSupport: {
type: Boolean,
default: false,
},
/**允许上传的文件拓展类型,默认 xls、xlsx */
fileExt: {
type: Array<string>,
default: ['xls', 'xlsx'],
},
/**上传文件大小单位MB默认 10 */
fileSize: {
type: Number,
default: 10,
},
});
/**上传状态 */
let updateState = reactive({
/**是否更新已经存在的数据 */
updateSupport: false,
/**是否上传中 */
loading: false,
/**是否已上传文件 */
isUpload: false,
/**上传结果信息 */
msg: '',
});
/**重置上传状态 */
function fnResetUpdateState() {
updateState = Object.assign(updateState, {
updateSupport: false,
loading: false,
isUpload: false,
msg: '',
});
}
/**上传前检查或转换压缩 */
function fnBeforeUpload(file: FileType) {
if (updateState.loading) return false;
const isAllowType = props.fileExt.some(v => file.name.endsWith(v));
if (!isAllowType) {
message.error(`只支持上传文件格式 ${props.fileExt.join('、')}`, 3);
}
const isLtM = file.size / 1024 / 1024 < props.fileSize;
if (!isLtM) {
message.error(`上传文件大小必须小于 ${props.fileSize}MB`, 3);
}
return isAllowType && isLtM;
}
/**上传请求发出 */
function fnUpload(up: UploadRequestOption) {
if (typeof props.uploadFileMethod !== 'function') return;
const hide = message.loading('正在上传并解析数据...', 0);
updateState.loading = true;
let formData = new FormData();
formData.append('file', up.file);
formData.append('updateSupport', `${updateState.updateSupport}`);
props
.uploadFileMethod(formData)
.then((res: ResultType) => {
updateState.loading = false;
updateState.isUpload = true;
updateState.msg = res.msg?.replaceAll(/<br\/>+/g, '\r');
})
.catch((err: { code: number; msg: string }) => {
message.error(`上传失败 ${err.msg}`);
})
.finally(() => {
hide();
});
}
/**弹框确认按钮事件 */
function fnModalOk() {
emit('update:visible', false);
emit('close', updateState.isUpload);
fnResetUpdateState();
}
/**弹框取消按钮事件 */
function fnModalCancel() {
emit('update:visible', false);
emit('close', updateState.isUpload);
fnResetUpdateState();
}
</script>
<template>
<a-modal
width="500px"
:title="props.title"
:visible="props.visible"
:keyboard="false"
:mask-closable="false"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-space :size="8" direction="vertical" style="width: 100%">
<a-upload-dragger
:disabled="updateState.loading"
name="file"
:max-count="1"
:show-upload-list="false"
:before-upload="fnBeforeUpload"
:custom-request="fnUpload"
>
<p class="ant-upload-drag-icon">
<inbox-outlined></inbox-outlined>
</p>
<p class="ant-upload-text">点击选择或将文件拖入边框区域进行上传</p>
<p class="ant-upload-hint">
仅允许导入
{{ props.fileExt.join('、') }}
格式文件上传文件大小
{{ props.fileSize }}
MB
</p>
</a-upload-dragger>
<a-row :gutter="18" justify="space-between" align="middle">
<a-col :span="12">
<a-checkbox
v-model:checked="updateState.updateSupport"
v-if="showUpdateSupport"
>
是否更新已经存在的数据
</a-checkbox>
</a-col>
<a-col :span="6">
<a-button
type="link"
title="下载模板"
@click="downloadTemplateMethod()"
v-if="downloadTemplateMethod"
>
下载模板
</a-button>
</a-col>
</a-row>
<a-textarea
:disabled="true"
:hidden="updateState.msg.length < 1"
:value="updateState.msg"
:auto-size="{ minRows: 2, maxRows: 8 }"
/>
</a-space>
</a-modal>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { ref } from 'vue';
const msg = ref<string>('愿这世间美好与你环环相扣');
</script>
<template>
<h1>{{ msg }}</h1>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,14 @@
<script lang="ts" setup>
import LinkiFrame from '@/components/LinkiFrame/index.vue';
import { ref } from 'vue';
const baseUrl = import.meta.env.VITE_API_BASE_URL;
const url = ref<string>(`${baseUrl}/swagger-ui/index.html`);
url.value = 'https://mask-api-midwayjs.apifox.cn/';
</script>
<template>
<LinkiFrame :src="url"></LinkiFrame>
</template>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,236 @@
<script lang="ts" setup>
import { reactive } from 'vue';
import { PageContainer } from '@ant-design-vue/pro-layout';
import { Modal } from 'ant-design-vue/lib/components';
import message from 'ant-design-vue/lib/message';
import { FileType, UploadFile } from 'ant-design-vue/lib/upload/interface';
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import saveAs from 'file-saver';
import {
downloadFile,
downloadFileChunk,
uploadFile,
uploadFileChunk,
} from '@/api/tool/file';
let state = reactive<{
/**上传状态 */
loading: boolean;
uploadFilePath: string;
downloadFilePath: string;
/*文件列表 */
fileList: UploadFile<any>[];
}>({
loading: false,
uploadFilePath: '',
downloadFilePath: '',
fileList: [
// {
// uid: '1',
// percent: 100,
// status: 'success',
// name: 'xxx.png',
// url: '/upload/default/2023/06/xxx.png',
// thumbUrl: '/upload/default/2023/06/xxx.png',
// },
],
});
/**下载文件 */
function fnDownload() {
const key = 'downloadFile';
message.loading({ content: '请稍等...', key });
const filePath = state.downloadFilePath;
if (!filePath) return;
downloadFile(filePath).then(res => {
if (res.code === 200) {
message.success({
content: `已完成下载`,
key,
duration: 2,
});
const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
saveAs(res.data, fileName);
} else {
message.error({
content: `${res.msg}`,
key,
duration: 2,
});
}
});
}
/**下载切片文件 */
function fnDownloadChunk() {
const key = 'downloadFileChunk';
message.loading({ content: '请稍等...', key });
const filePath = state.downloadFilePath;
downloadFileChunk(filePath, 5).then(blob => {
console.log(blob);
if (blob.size === 0) {
message.error({
content: `文件读取失败`,
key,
duration: 2,
});
} else {
message.success({
content: `已完成下载`,
key,
duration: 2,
});
const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
saveAs(blob, fileName);
}
});
}
/**上传前检查或转换压缩 */
function fnBeforeUpload(file: FileType) {
if (state.loading) return false;
const isJpgOrPng = ['image/jpeg', 'image/png'].includes(file.type);
if (!isJpgOrPng) {
message.error('只支持上传图片格式jpg、png', 3);
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片文件大小必须小于 2MB', 3);
}
return isJpgOrPng && isLt2M;
}
/**上传文件 */
function fnUpload(up: UploadRequestOption) {
Modal.confirm({
title: '提示',
content: `确认要上传文件吗?`,
onOk() {
// 发送请求
const hide = message.loading('请稍等...', 0);
state.loading = true;
let formData = new FormData();
formData.append('file', up.file);
formData.append('subPath', 'default');
uploadFile(formData).then(res => {
state.loading = false;
hide();
if (res.code === 200) {
message.success('文件上传成功', 3);
state.uploadFilePath = res.data.url;
state.downloadFilePath = res.data.fileName;
} else {
message.error(res.msg, 3);
}
});
},
});
}
/**上传分片 */
function fnUploadChunk(up: UploadRequestOption) {
const fileData = up.file as File;
const item = state.fileList.find(f => f.name === fileData.name);
Modal.confirm({
title: '提示',
content: `确认要上传文件吗?`,
onOk() {
// 发送请求
const hide = message.loading('请稍等...', 0);
uploadFileChunk(fileData, 4, 'default').then(res => {
hide();
if (res.code === 200) {
message.success('文件上传成功', 3);
if (item) {
item.url = res.data.url;
item.name = res.data.newFileName;
item.percent = 100;
item.status = 'done';
}
} else {
message.error(res.msg, 3);
state.fileList.splice(state.fileList.length - 1, 1);
}
});
},
onCancel() {
if (item) {
state.fileList.splice(state.fileList.length - 1, 1);
}
},
});
}
</script>
<template>
<PageContainer title="上传示例">
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-card title="普通文件" style="margin-bottom: 16px">
<a-row :gutter="8">
<a-col :span="24" style="margin-bottom: 16px">
<a-input
style="margin-bottom: 16px"
type="text"
placeholder="输入资源文件地址"
v-model:value="state.downloadFilePath"
>
<template #suffix>
<a-button type="primary" @click="fnDownload">
普通下载
</a-button>
</template>
</a-input>
<a-input
type="text"
placeholder="输入资源文件地址"
v-model:value="state.downloadFilePath"
>
<template #suffix>
<a-button type="primary" @click="fnDownloadChunk">
分片下载
</a-button>
</template>
</a-input>
</a-col>
<a-col :span="24">
<a-space direction="vertical" :size="16">
<a-upload
name="file"
list-type="picture"
:max-count="1"
:show-upload-list="false"
:before-upload="fnBeforeUpload"
:custom-request="fnUpload"
>
<a-button type="default" :loading="state.loading">
选择文件
</a-button>
</a-upload>
<a-image
:width="128"
:height="128"
:src="state.uploadFilePath"
/>
</a-space>
</a-col>
</a-row>
</a-card>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-card title="大文件分片上传" style="margin-bottom: 16px">
<a-upload
v-model:file-list="state.fileList"
name="file"
list-type="picture"
:custom-request="fnUploadChunk"
>
<a-button> 选择文件 </a-button>
</a-upload>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped></style>