前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题

This commit is contained in:
JEECG
2024-06-23 10:39:52 +08:00
parent bb918b742e
commit 0325e34dcb
1439 changed files with 171106 additions and 0 deletions

View File

@ -0,0 +1,19 @@
import { defHttp } from '/@/utils/http/axios';
export enum Api {
list = '/sys/user/queryByOrgCodeForAddressList',
positionList = '/sys/position/list',
queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync',
}
/**
* 获取部门树列表
*/
export const queryDepartTreeSync = (params?) => defHttp.get({ url: Api.queryDepartTreeSync, params });
/**
* 部门用户信息
*/
export const list = (params?) => defHttp.get({ url: Api.list, params });
/**
* 职务list
*/
export const positionList = (params?) => defHttp.get({ url: Api.positionList, params });

View File

@ -0,0 +1,51 @@
import { FormSchema } from '/@/components/Form';
import { BasicColumn } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '姓名',
dataIndex: 'realname',
width: 150,
},
{
title: '工号',
dataIndex: 'workNo',
width: 100,
},
{
title: '部门',
dataIndex: 'departName',
width: 200,
},
{
title: '职务',
dataIndex: 'post',
width: 150,
slots: { customRender: 'post' },
},
{
title: '手机',
width: 150,
dataIndex: 'telephone',
},
{
title: '邮箱',
width: 150,
dataIndex: 'email',
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '姓名',
field: 'realname',
component: 'Input',
colProps: { span: 6 },
},
{
label: '工号',
field: 'workNo',
component: 'Input',
colProps: { span: 6 },
},
];

View File

@ -0,0 +1,158 @@
<template>
<a-card :bordered="false" style="height: 100%">
<a-spin :spinning="loading">
<a-input-search placeholder="按部门名称搜索…" style="margin-bottom: 10px" @search="onSearch" allowClear />
<!--组织机构树-->
<template v-if="treeData.length > 0">
<a-tree
v-if="!treeReloading"
showLine
:clickRowToExpand="false"
:treeData="treeData"
:selectedKeys="selectedKeys"
:load-data="loadChildrenTreeData"
v-model:expandedKeys="expandedKeys"
@select="onSelect"
></a-tree>
</template>
<a-empty v-else description="暂无数据" />
</a-spin>
</a-card>
</template>
<script lang="ts" setup>
import { inject, nextTick, ref, unref } from 'vue';
import { queryDepartTreeSync } from '../address.api';
import { searchByKeywords } from '/@/views/system/departUser/depart.user.api';
import { Popconfirm } from 'ant-design-vue';
const prefixCls = inject('prefixCls');
const emit = defineEmits(['select', 'rootTreeData']);
const loading = ref<boolean>(false);
// 部门树列表数据
const treeData = ref<any[]>([]);
// 当前展开的项
const expandedKeys = ref<any[]>([]);
// 当前选中的项
const selectedKeys = ref<any[]>([]);
// 树组件重新加载
const treeReloading = ref<boolean>(false);
// 当前选中的部门
const currentDepart = ref<any>(null);
// 搜索关键字
const searchKeyword = ref('');
// 加载顶级部门信息
async function loadRootTreeData() {
try {
loading.value = true;
treeData.value = [];
const result = await queryDepartTreeSync();
if (Array.isArray(result)) {
treeData.value = result;
}
if (expandedKeys.value.length === 0) {
autoExpandParentNode();
}
} finally {
loading.value = false;
}
}
loadRootTreeData();
// 加载子级部门信息
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
pid: treeNode.dataRef.id,
});
if (result.length == 0) {
treeNode.dataRef.isLeaf = true;
} else {
treeNode.dataRef.children = result;
if (expandedKeys.value.length > 0) {
// 判断获取的子级是否有当前展开的项
let subKeys: any[] = [];
for (let key of expandedKeys.value) {
if (result.findIndex((item) => item.id === key) !== -1) {
subKeys.push(key);
}
}
if (subKeys.length > 0) {
expandedKeys.value = [...expandedKeys.value];
}
}
}
treeData.value = [...treeData.value];
} catch (e) {
console.error(e);
}
return Promise.resolve();
}
// 自动展开父节点,只展开一级
function autoExpandParentNode() {
let item = treeData.value[0];
if (item) {
if (!item.isLeaf) {
expandedKeys.value = [item.key];
}
reloadTree();
}
}
// 重新加载树组件,防止无法默认展开数据
async function reloadTree() {
await nextTick();
treeReloading.value = true;
await nextTick();
treeReloading.value = false;
}
/**
* 设置当前选中的行
*/
function setSelectedKey(key: string, data?: object) {
selectedKeys.value = [key];
if (data) {
currentDepart.value = data;
emit('select', data);
}
}
// 搜索事件
async function onSearch(value: string) {
if (value) {
try {
loading.value = true;
treeData.value = [];
let result = await searchByKeywords({ keyWord: value });
if (Array.isArray(result)) {
treeData.value = result;
}
autoExpandParentNode();
} finally {
loading.value = false;
}
} else {
loadRootTreeData();
}
searchKeyword.value = value;
}
// 树选择事件
function onSelect(selKeys, event) {
if (selKeys.length > 0 && selectedKeys.value[0] !== selKeys[0]) {
setSelectedKey(selKeys[0], event.selectedNodes[0]);
} else {
// 这样可以防止用户取消选择
setSelectedKey(selectedKeys.value[0]);
}
}
defineExpose({
loadRootTreeData,
});
</script>

View File

@ -0,0 +1,13 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-address-list';
.@{prefix-cls} {
// update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
background-color: @component-background;
// update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
&--box {
.ant-tabs-nav {
padding: 0 20px;
}
}
}

View File

@ -0,0 +1,89 @@
<template>
<a-row :class="['p-4', `${prefixCls}--box`]" type="flex" :gutter="10" style="max-height: 800px">
<a-col :xl="6" :lg="24" :md="24" style="margin-bottom: 10px">
<DepartLeftTree ref="leftTree" @select="onTreeSelect" />
</a-col>
<a-col :xl="18" :lg="24" :md="24" style="margin-bottom: 10px">
<div style="height: 100%;" class="address-book">
<!--引用表格-->
<BasicTable @register="registerTable">
<template #post="{ text }">
{{
(text || '')
.split(',')
.map((t) => (positionInfo[t] ? positionInfo[t] : t))
.join(',')
}}
</template>
</BasicTable>
</div>
</a-col>
</a-row>
</template>
<script lang="ts" setup>
import { provide, ref, unref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import DepartLeftTree from './components/DepartLeftTree.vue';
import { BasicTable } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns, searchFormSchema } from './address.data';
import { list, positionList } from './address.api';
const { prefixCls } = useDesign('address-list');
provide('prefixCls', prefixCls);
// 给子组件定义一个ref变量
const leftTree = ref();
// 当前选中的部门code
const orgCode = ref('');
const positionInfo = ref({});
// 列表页面公共参数、方法
const { tableContext } = useListPage({
tableProps: {
api: list,
columns,
//update-begin---author:wangshuai ---date:20220629 for[VUEN-1485]进入系统管理--通讯录页面后,网页命令行报错------------
rowKey: 'userId',
//update-end---author:wangshuai ---date:20220629 for[VUEN-1485]进入系统管理--通讯录页面后,网页命令行报错--------------
showIndexColumn: true,
formConfig: {
schemas: searchFormSchema,
},
canResize: false,
actionColumn: null,
showTableSetting: false,
// 请求之前对参数做处理
beforeFetch(params) {
params.orgCode = orgCode.value;
},
},
});
//注册table数据
const [registerTable, { reload }] = tableContext;
// 左侧树选择后触发
function onTreeSelect(data) {
orgCode.value = data.orgCode;
reload();
}
// 查询职务信息
async function queryPositionInfo() {
const result = await positionList({ pageSize: 99999 });
if (result) {
let obj = {};
result.records.forEach((position) => {
obj[position['id']] = position['name'];
});
positionInfo.value = obj;
}
}
queryPositionInfo();
</script>
<style lang="less">
@import './index.less';
</style>

View File

@ -0,0 +1,69 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
//第三方登录配置
addThirdAppConfig = '/sys/thirdApp/addThirdAppConfig',
editThirdAppConfig = '/sys/thirdApp/editThirdAppConfig',
getThirdConfigByTenantId = '/sys/thirdApp/getThirdConfigByTenantId',
syncDingTalkDepartUserToLocal = '/sys/thirdApp/sync/dingtalk/departAndUser/toLocal',
getThirdUserByWechat = '/sys/thirdApp/getThirdUserByWechat',
wechatEnterpriseToLocal = '/sys/thirdApp/sync/wechatEnterprise/departAndUser/toLocal',
getThirdUserBindByWechat = '/sys/thirdApp/getThirdUserBindByWechat',
deleteThirdAccount = '/sys/thirdApp/deleteThirdAccount',
}
/**
* 第三方配置保存或者更新
*/
export const saveOrUpdateThirdConfig = (params, isUpdate) => {
let url = isUpdate ? Api.editThirdAppConfig : Api.addThirdAppConfig;
return defHttp.post({ url: url, params }, { joinParamsToUrl: true });
};
/**
* 获取第三方配置
* @param params
*/
export const getThirdConfigByTenantId = (params) => {
return defHttp.get({ url: Api.getThirdConfigByTenantId, params });
};
/**
* 同步钉钉部门用户到本地
* @param params
*/
export const syncDingTalkDepartUserToLocal = () => {
return defHttp.get({ url: Api.syncDingTalkDepartUserToLocal, timeout: 60000 }, { isTransformResponse: false });
};
/**
* 获取企业微信绑定的用户信息
* @param params
*/
export const getThirdUserByWechat = () => {
return defHttp.get({ url: Api.getThirdUserByWechat }, { isTransformResponse: false });
};
/**
* 同步企业微信用户部门到本地
* @param params
*/
export const wechatEnterpriseToLocal = (params) => {
return defHttp.get({ url: Api.wechatEnterpriseToLocal, params }, { isTransformResponse: false });
};
/**
* 获取绑定企业微信的用户
* @param params
*/
export const getThirdUserBindByWechat = () => {
return defHttp.get({ url: Api.getThirdUserBindByWechat }, { isTransformResponse: false });
};
/**
* 根据第三方账号表的id解绑账号
* @param params
*/
export const deleteThirdAccount = (params) => {
return defHttp.delete({ url: Api.deleteThirdAccount, params }, { isTransformResponse:false, joinParamsToUrl: true });
};

View File

@ -0,0 +1,58 @@
//第三方app配置表单
import { FormSchema } from '/@/components/Form';
//第三方app表单
export const thirdAppFormSchema: FormSchema[] = [
{
label: 'id',
field: 'id',
component: 'Input',
show: false,
},
{
label: 'thirdType',
field: 'thirdType',
component: 'Input',
show: false,
},
{
label: 'Agentld',
field: 'agentId',
component: 'Input',
required: true,
},
{
label: 'AppKey',
field: 'clientId',
component: 'Input',
required: true,
},
{
label: 'AppSecret',
field: 'clientSecret',
component: 'Input',
required: true,
},
{
label: 'agentAppSecret',
field: 'agentAppSecret',
component: 'Input',
ifShow: false,
},{
label: '启用',
field: 'status',
component: 'Switch',
componentProps:{
checkedChildren:'关闭',
checkedValue:1,
unCheckedChildren:'开启',
unCheckedValue: 0
},
defaultValue: 1
},{
label: '租户id',
field: 'tenantId',
component: 'Input',
show: false,
},
];

View File

@ -0,0 +1,316 @@
<!--弹窗绑定企业微信页面-->
<template>
<BasicModal @register="registerModal" :width="800" :title="title" destroyOnClose>
<a-spin :spinning="loading">
<div class="we-bind">
<a-row :span="24" class="we-title-background">
<a-col :span="12" class="border-right">
<span>组织用户</span>
</a-col>
<a-col :span="12" class="padding-left">
<span>企业微信用户</span>
</a-col>
</a-row>
<a-row :span="24">
<template v-for="(item, index) in bindData.jwUserDepartVos">
<a-col :span="12" class="border-right padding-left border-bottom">
<div class="we-account">
<a-avatar v-if="item.avatar" :src="getFileAccessHttpUrl(item.avatar)" :size="28"></a-avatar>
<a-avatar v-else :size="28">
{{ item.realName.length > 2 ? item.realName.substr(0, 2) : item.realName }}
</a-avatar>
<a-input style="margin-left: 20px" :value="item.realName" readonly />
</div>
</a-col>
<a-col :span="12" class="padding-left border-bottom">
<div class="we-account">
<span v-if="item.wechatUserId || izBind" class="we-remove"
>{{ item.wechatRealName }} <span style="margin-right: 20px" @click="handleRemoveClick(index, item)">移出</span></span
>
<a-select
v-else
v-model:value="item.wechatUserId"
:options="userList"
:fieldNames="{ label: 'wechatRealName', value: 'wechatUserId' }"
style="width: 200px"
showSearch
@select="(val, option) => handleSelect(val, option, index)"
/>
</div>
</a-col>
</template>
</a-row>
</div>
</a-spin>
<template #footer>
<a-button v-if="!izBind" type="primary" @click="handleSubmit">同步</a-button>
</template>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, h, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { getThirdUserByWechat, wechatEnterpriseToLocal, getThirdUserBindByWechat, deleteThirdAccount } from './ThirdApp.api';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { useMessage } from '@/hooks/web/useMessage';
import { Modal } from 'ant-design-vue';
import { useUserStore } from '@/store/modules/user';
export default defineComponent({
name: 'ThirdAppBindWeEnterpriseModal',
components: { BasicModal },
setup(props, { emit }) {
const title = ref<string>('企业微信绑定');
//企业微信的绑定数据
const bindData = ref<any>({});
const loading = ref<boolean>(false);
const btnLoading = ref<boolean>(false);
const { createMessage } = useMessage();
const userList = ref<any>([]);
//同步文本信息展示
const syncText = ref<string>('');
//是否已绑定数据,展示不同的列表
const izBind = ref<boolean>(false);
const userStore = useUserStore();
//表单赋值
const [registerModal, { closeModal }] = useModalInner(async (data) => {
loading.value = true;
console.log('izBind', izBind);
if (!data.izBind) {
await getUnboundData();
} else {
await getBoundData();
}
izBind.value = data.izBind;
});
/**
* 未绑定的数据
*/
async function getUnboundData() {
await getThirdUserByWechat().then((res) => {
if (res.success) {
let userLists = res.result.userList;
bindData.value = res.result;
userList.value = res.result.userList;
/* if (userLists && userLists.length > 0) {
syncText.value = "";
} else {
syncText.value = "企业微信用户均已同步";
}*/
loading.value = false;
} else {
createMessage.warning(res.message);
loading.value = false;
}
});
}
/**
* 已绑定的数据
*/
async function getBoundData() {
await getThirdUserBindByWechat().then((res) => {
if (res.success) {
bindData.value.jwUserDepartVos = res.result;
loading.value = false;
} else {
createMessage.warn(res.message);
loading.value = false;
}
});
}
/**
* 第三方配置点击事件
*/
async function handleSubmit() {
btnLoading.value = true;
let userList = bindData.value.userList;
//重新封装数据只留用户id和企业微信id即可,还需要把没绑定的用户传给后台
let params: any = [];
//查询用户绑定的企业微信用户
for (const item of bindData.value.jwUserDepartVos) {
if (item.wechatUserId) {
userList = userList.filter((a) => a.wechatUserId != item.wechatUserId);
params.push({
wechatUserId: item.wechatUserId,
wechatDepartId: item.wechatDepartId,
wechatRealName: item.wechatRealName,
userId: item.userId,
});
}
}
let text: string = '';
//查询未被绑定的租户
if (userList && userList.length > 0) {
for (const item of userList) {
params.push({ wechatUserId: item.wechatUserId, wechatDepartId: item.wechatDepartId, wechatRealName: item.wechatRealName });
}
text = '检测到未绑定的企业微信用户 ' + userList.length + ' 位,平台将会为这 ' + userList.length + ' 位用户创建新的账号';
}
Modal.confirm({
title: '确认同步',
content: text,
okText: '确认',
onOk: () => {
let json = JSON.stringify(params);
console.log('json', json);
wechatEnterpriseToLocal({ jwUserDepartJson: json })
.then((res) => {
let options = {};
if (res.success) {
if (res.result) {
options = {
width: 600,
title: res.message,
content: () => {
let nodes;
let successInfo = [`成功信息如下:`, renderTextarea(h, res.result.successInfo.map((v, i) => `${i + 1}. ${v}`).join('\n'))];
if (res.success) {
nodes = [...successInfo, h('br'), `无失败信息!`];
} else {
nodes = [
`失败信息如下:`,
renderTextarea(h, res.result.failInfo.map((v, i) => `${i + 1}. ${v}`).join('\n')),
h('br'),
...successInfo,
];
}
return nodes;
},
};
}
closeModal();
emit('success', options, res);
}
})
.finally(() => {
btnLoading.value = false;
});
},
});
}
/**
* 下拉框选择事件
*/
function handleSelect(val, option, index) {
bindData.value.jwUserDepartVos[index].wechatUserId = option.wechatUserId;
bindData.value.jwUserDepartVos[index].wechatRealName = option.wechatRealName;
bindData.value.jwUserDepartVos[index].wechatDepartId = option.wechatDepartId;
userList.value = userList.value.filter((item) => item.wechatUserId != option.wechatUserId);
}
/**
* 移出事件
* @param index
* @param item
*/
function handleRemoveClick(index, item) {
if (!izBind.value) {
userList.value.push({
wechatUserId: item.wechatUserId,
wechatRealName: item.wechatRealName,
wechatDepartId: item.wechatDepartId,
});
bindData.value.jwUserDepartVos[index].wechatUserId = '';
bindData.value.jwUserDepartVos[index].wechatRealName = '';
bindData.value.jwUserDepartVos[index].wechatDepartId = '';
} else {
Modal.confirm({
title: '确认取消绑定吗',
okText: '确认',
onOk: async () => {
await deleteThirdAccount({ id: item.thirdId, sysUserId: userStore.getUserInfo.id }).then((res) => {
if (res.success) {
createMessage.success('取消绑定成功!');
getBoundData();
} else {
createMessage.warning(res.message);
}
});
},
});
}
}
function renderTextarea(h, value) {
return h(
'div',
{
id: 'box',
style: {
minHeight: '100px',
border: '1px solid #d9d9d9',
fontSize: '14px',
maxHeight: '250px',
whiteSpace: 'pre',
overflow: 'auto',
padding: '10px',
},
},
value
);
}
return {
title,
registerModal,
handleSubmit,
bindData,
getFileAccessHttpUrl,
loading,
userList,
handleSelect,
handleRemoveClick,
btnLoading,
izBind,
};
},
});
</script>
<style lang="less" scoped>
.we-bind {
overflow-y: auto;
border: 1px @border-color-base solid;
border-bottom: none;
.we-title-background {
background: @component-background;
height: 40px;
line-height: 40px;
padding: 0 10px;
}
.we-account {
display: flex;
height: 40px;
line-height: 40px;
align-items: center;
}
:deep(.ant-input) {
border: none;
padding: 0;
box-shadow: none;
}
.we-remove {
display: flex;
justify-content: space-between;
width: 100%;
cursor: pointer;
}
.border-right {
border-right: 1px @border-color-base solid;
}
.border-bottom {
border-bottom: 1px @border-color-base solid;
}
.padding-left {
padding-left: 10px;
}
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<div class="ding-ding-container" :class="[`${prefixCls}`]">
<div class="ding-header">
<ul class="ding-menu-tab">
<li :class="activeKey === 'ding' ? 'active' : ''" @click="dingLiClick('ding')"><a>钉钉集成</a></li>
<li :class="activeKey === 'wechat' ? 'active' : ''" @click="dingLiClick('wechat')"><a>企业微信集成</a></li>
</ul>
</div>
<div v-show="activeKey === 'ding'" class="base-collapse">
<ThirdAppDingTalkConfigForm />
</div>
<div v-show="activeKey === 'wechat'" class="base-collapse">
<ThirdAppWeEnterpriseConfigForm />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import ThirdAppDingTalkConfigForm from './ThirdAppDingTalkConfigForm.vue';
import ThirdAppWeEnterpriseConfigForm from './ThirdAppWeEnterpriseConfigForm.vue';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'ThirdAppConfigList',
components: {
ThirdAppDingTalkConfigForm,
ThirdAppWeEnterpriseConfigForm,
},
setup() {
const { prefixCls } = useDesign('j-dd-container');
//选中的key
const activeKey = ref<string>('ding');
/**
* tab点击事件
* @param key
*/
function dingLiClick(key) {
activeKey.value = key;
}
return {
activeKey,
dingLiClick,
prefixCls,
};
},
});
</script>
<style lang="less" scoped>
.ding-ding-container {
border-radius: 4px;
height: calc(100% - 80px);
margin: 16px;
}
.ding-header {
align-items: center;
/*begin 兼容暗夜模式*/
border-bottom: 1px solid @border-color-base;
/*end 兼容暗夜模式*/
box-sizing: border-box;
display: flex;
height: 50px;
justify-content: space-between;
padding: 0 24px;
ul {
margin-bottom: 0;
}
}
.ding-menu-tab {
display: flex;
height: 100%;
li {
align-items: center;
border-bottom: 2px solid transparent;
display: flex;
height: 100%;
margin-right: 38px;
a {
/*begin 兼容暗夜模式*/
color: @text-color !important;
/*end 兼容暗夜模式*/
font-size: 15px;
font-weight: 700;
}
}
}
.active {
border-bottom-color: #2196f3 !important;
a {
color: #333 !important;
}
}
.empty-image{
align-items: center;
display: flex;
flex-direction: column;
height: calc(100% - 50px);
justify-content: center;
width: 100%;
}
</style>
<style lang="less">
/* update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效*/
@prefix-cls: ~'@{namespace}-j-dd-container';
/*begin 兼容暗夜模式*/
.@{prefix-cls} {
background: @component-background;
.ding-header {
border-bottom: 1px solid @border-color-base;
}
.ding-menu-tab {
li {
a {
color: @text-color !important;
}
}
}
.ant-collapse-borderless {
background-color: @component-background;
}
.ant-collapse{
background-color: @component-background;
}
}
/*end 兼容暗夜模式*/
/* update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效*/
</style>

View File

@ -0,0 +1,69 @@
<template>
<BasicModal @register="registerModal" :width="800" :title="title" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { useForm, BasicForm } from '/@/components/Form';
import { thirdAppFormSchema } from './ThirdApp.data';
import { getThirdConfigByTenantId, saveOrUpdateThirdConfig } from './ThirdApp.api';
export default defineComponent({
name: 'ThirdAppConfigModal',
components: { BasicModal, BasicForm },
setup(props, { emit }) {
const title = ref<string>('钉钉配置');
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: thirdAppFormSchema,
showActionButtonGroup: false,
labelCol: { span: 24 },
wrapperCol: { span: 24 },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ confirmLoading: true });
if (data.thirdType == 'dingtalk') {
title.value = '钉钉配置';
} else {
title.value = '企业微信配置';
}
//重置表单
await resetFields();
let values = await getThirdConfigByTenantId({ tenantId: data.tenantId, thirdType: data.thirdType });
setModalProps({ confirmLoading: false });
//表单赋值
if (values) {
await setFieldsValue(values);
} else {
await setFieldsValue(data);
}
});
/**
* 第三方配置点击事件
*/
async function handleSubmit() {
let values = await validate();
let isUpdate = false;
if (values.id) {
isUpdate = true;
}
await saveOrUpdateThirdConfig(values, isUpdate);
emit('success');
closeModal();
}
return {
title,
registerForm,
registerModal,
handleSubmit,
};
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,297 @@
<template>
<div class="base-collapse">
<div class="header"> 钉钉集成 </div>
<a-collapse expand-icon-position="right" :bordered="false">
<a-collapse-panel key="1">
<template #header>
<div style="font-size: 16px"> 1.获取对接信息</div>
</template>
<div class="base-desc">从钉钉开放平台获取对接信息,即可开始集成以及同步通讯录</div>
<div style="margin-top: 5px">
<a href='https://help.qiaoqiaoyun.com/expand/dingding.html' target='_blank'>如何获取对接信息?</a>
</div>
</a-collapse-panel>
</a-collapse>
<div class="sync-padding">
<a-collapse expand-icon-position="right" :bordered="false">
<a-collapse-panel key="2">
<template #header>
<div style="width: 100%; justify-content: space-between; display: flex">
<div style="font-size: 16px"> 2.对接信息录入</div>
</div>
</template>
<div class="base-desc">完成步骤1后填入Agentld、 AppKey、AppSecret后 可对接应用与同步通讯录</div>
<div class="flex-flow">
<div class="base-title">Agentld</div>
<div class="base-message">
<a-input-password v-model:value="appConfigData.agentId" readonly />
</div>
</div>
<div class="flex-flow">
<div class="base-title">AppKey</div>
<div class="base-message">
<a-input-password v-model:value="appConfigData.clientId" readonly />
</div>
</div>
<div class="flex-flow">
<div class="base-title">AppSecret</div>
<div class="base-message">
<a-input-password v-model:value="appConfigData.clientSecret" readonly />
</div>
</div>
<div style="margin-top: 20px; width: 100%; text-align: right">
<a-button @click="dingEditClick">编辑</a-button>
</div>
</a-collapse-panel>
</a-collapse>
<div class="sync-padding">
<div style="font-size: 16px; width: 100%"> 3.数据同步</div>
<div style="margin-top: 20px" class="base-desc">
从钉钉同步到本地
<ul style='list-style-type: disc;margin-left: 20px;'>
<li>同步部门到本地</li>
<li>
同步部门下的用户到本地
<a-tooltip title='同步用户与部门文档'>
<a-icon @click='handleIconClick' type="question-circle" class="sync-text"/>
</a-tooltip>
</li>
</ul>
<div style="float: right">
<a-button :loading="btnLoading" @click="syncDingTalk">{{ !btnLoading ? '同步' : '同步中' }}</a-button>
</div>
</div>
</div>
</div>
</div>
<ThirdAppConfigModal @register="registerAppConfigModal" @success="handleSuccess" />
</template>
<script lang="ts">
import { defineComponent, h, inject, onMounted, reactive, ref, watch } from 'vue';
import { getThirdConfigByTenantId, syncDingTalkDepartUserToLocal } from './ThirdApp.api';
import { useModal } from '/@/components/Modal';
import ThirdAppConfigModal from './ThirdAppConfigModal.vue';
import { Modal } from 'ant-design-vue';
import { getTenantId } from '/@/utils/auth';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
name: 'OrganDingConfigForm',
components: {
ThirdAppConfigModal,
},
setup() {
const { createMessage } = useMessage();
//折叠面板选中key
const collapseActiveKey = ref<string>('');
//按钮加载事件
const btnLoading = ref<boolean>(false);
//第三方配置数据
const appConfigData = ref<any>({
agentId: undefined,
clientId: '',
clientSecret: '',
});
//企业微信钉钉配置modal
const [registerAppConfigModal, { openModal }] = useModal();
/**
* 钉钉编辑
*/
async function dingEditClick() {
let tenantId = getTenantId();
openModal(true, {
tenantId: tenantId,
thirdType: 'dingtalk',
});
}
/**
* 初始化第三方数据
*/
async function initThirdAppConfigData(params) {
let values = await getThirdConfigByTenantId(params);
if (values) {
appConfigData.value = values;
}
}
/**
* 成功回调
*/
function handleSuccess() {
let tenantId = getTenantId();
initThirdAppConfigData({ tenantId: tenantId, thirdType: 'dingtalk' });
}
/**
* 同步钉钉
*/
async function syncDingTalk() {
btnLoading.value = true;
await syncDingTalkDepartUserToLocal()
.then((res) => {
let options = {};
if (res.result) {
options = {
width: 600,
title: res.message,
content: () => {
let nodes;
let successInfo = [`成功信息如下:`, renderTextarea(h, res.result.successInfo.map((v, i) => `${i + 1}. ${v}`).join('\n'))];
if (res.success) {
nodes = [...successInfo, h('br'), `无失败信息!`];
} else {
nodes = [
`失败信息如下:`,
renderTextarea(h, res.result.failInfo.map((v, i) => `${i + 1}. ${v}`).join('\n')),
h('br'),
...successInfo,
];
}
return nodes;
},
};
}
if (res.success) {
if (options != null) {
Modal.success(options);
} else {
createMessage.warning(res.message);
}
} else {
if (options && options.title) {
Modal.warning(options)
} else {
createMessage.warning({
content: "同步失败请检查对接信息录入中是否填写正确并确认是否已开启钉钉配置",
duration: 5
});
}
}
})
.finally(() => {
btnLoading.value = false;
});
}
/**
* 渲染文本
* @param h
* @param value
*/
function renderTextarea(h, value) {
return h(
'div',
{
id: 'box',
style: {
minHeight: '100px',
border: '1px solid #d9d9d9',
fontSize: '14px',
maxHeight: '250px',
whiteSpace: 'pre',
overflow: 'auto',
padding: '10px',
},
},
value
);
}
/**
* 钉钉同步文档
*/
function handleIconClick(){
window.open("https://help.qiaoqiaoyun.com/expand/dingdingsyn.html","_target")
}
onMounted(() => {
let tenantId = getTenantId();
initThirdAppConfigData({ tenantId: tenantId, thirdType: 'dingtalk' });
});
return {
appConfigData,
collapseActiveKey,
registerAppConfigModal,
dingEditClick,
handleSuccess,
syncDingTalk,
btnLoading,
handleIconClick,
};
},
});
</script>
<style lang="less" scoped>
.header {
align-items: center;
box-sizing: border-box;
display: flex;
height: 50px;
justify-content: space-between;
font-weight: 700;
font-size: 18px;
color: @text-color;
}
.flex-flow {
display: flex;
min-height: 0;
}
.sync-padding {
padding: 12px 0 16px;
color: @text-color;
}
.base-collapse {
margin-top: 20px;
padding: 0 24px;
font-size: 20px;
.base-desc {
font-size: 14px;
}
.base-title {
width: 100px;
text-align: left;
height: 50px;
line-height: 50px;
}
.base-message {
width: 100%;
height: 50px;
line-height: 50px;
}
:deep(.ant-collapse-header) {
padding: 12px 0 16px;
}
:deep(.ant-collapse-content-box) {
padding-left: 0;
}
}
/*begin 兼容暗夜模式*/
//暗黑模式下卡片的边框设置成none
[data-theme='dark'] .base-collapse .ant-collapse{
border: none !important;
}
/*end 兼容暗夜模式*/
/*文档按钮问号样式*/
.sync-text{
margin-left: 2px;
cursor: pointer;
position: relative;
top: 2px
}
:deep(.ant-collapse-borderless >.ant-collapse-item:last-child) {border-bottom-width:1px;}
</style>

View File

@ -0,0 +1,250 @@
<template>
<div class="base-collapse">
<div class="header"> 企业微信集成 </div>
<a-collapse expand-icon-position="right" :bordered="false">
<a-collapse-panel key="1">
<template #header>
<div style="font-size: 16px"> 1.获取对接信息</div>
</template>
<div class="base-desc">从企业微信平台获取对接信息,即可开始集成以及同步通讯录</div>
<div style="margin-top: 5px">
<a href="https://help.qiaoqiaoyun.com/expand/dingding.html" target="_blank">如何获取对接信息?</a>
</div>
</a-collapse-panel>
</a-collapse>
<div>
<a-collapse expand-icon-position="right" :bordered="false">
<a-collapse-panel key="2">
<template #header>
<div style="width: 100%; justify-content: space-between; display: flex">
<div style="font-size: 16px"> 2.对接信息录入</div>
</div>
</template>
<div class="flex-flow">
<div class="base-title">Agentld</div>
<div class="base-message">
<a-input-password v-model:value="appConfigData.agentId" readonly />
</div>
</div>
<div class="flex-flow">
<div class="base-title">AppKey</div>
<div class="base-message">
<a-input-password v-model:value="appConfigData.clientId" readonly />
</div>
</div>
<div class="flex-flow">
<div class="base-title">AppSecret</div>
<div class="base-message">
<a-input-password v-model:value="appConfigData.clientSecret" readonly />
</div>
</div>
<div style="margin-top: 20px; width: 100%; text-align: right">
<a-button @click="weEnterpriseEditClick">编辑</a-button>
</div>
</a-collapse-panel>
</a-collapse>
<div class="sync-padding">
<div style="font-size: 16px; width: 100%"> 3.数据同步</div>
<div style="margin-top: 20px" class="base-desc">
从企业微信同步到敲敲云
<a style="margin-left: 10px" @click="seeBindWeChat">查看已绑定的企业微信用户</a>
<div style="float: right">
<a-button @loading="btnLoading" @click="thirdUserByWechat">同步</a-button>
</div>
</div>
</div>
</div>
</div>
<ThirdAppConfigModal @register="registerAppConfigModal" @success="handleSuccess" />
<ThirdAppBindWeEnterpriseModal @register="registerBindAppConfigModal" @success="handleBindSuccess" />
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { getThirdConfigByTenantId } from './ThirdApp.api';
import ThirdAppConfigModal from './ThirdAppConfigModal.vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { getTenantId } from '@/utils/auth';
import ThirdAppBindWeEnterpriseModal from './ThirdAppBindWeEnterpriseModal.vue';
import { Modal } from "ant-design-vue";
export default defineComponent({
name: 'ThirdAppWeEnterpriseConfigForm',
components: {
ThirdAppConfigModal,
ThirdAppBindWeEnterpriseModal,
},
setup() {
const btnLoading = ref<boolean>(false);
//第三方配置数据
const appConfigData = ref<any>({
agentId: '',
clientId: '',
clientSecret: '',
agentAppSecret: '',
});
//企业微信钉钉配置modal
const [registerAppConfigModal, { openModal }] = useModal();
const [registerBindAppConfigModal, { openModal: openBindModal }] = useModal();
const { createMessage } = useMessage();
/**
* 初始化数据
*
* @param params
*/
async function initThirdAppConfigData(params) {
let values = await getThirdConfigByTenantId(params);
if (values) {
appConfigData.value = values;
}
}
/**
* 企业微信编辑
*/
async function weEnterpriseEditClick() {
let tenantId = getTenantId();
openModal(true, {
tenantId: tenantId,
thirdType: 'wechat_enterprise',
});
}
/**
* 获取企业微信绑定的用户
*/
async function thirdUserByWechat() {
openBindModal(true, { izBind: false });
}
/**
* 成功回调
*/
function handleSuccess() {
let tenantId = getTenantId();
initThirdAppConfigData({ tenantId: tenantId, thirdType: 'wechat_enterprise' });
}
/**
* 绑定成功返回值
*
* @param options
* @param item
*/
function handleBindSuccess(options, item) {
console.log("options:::",options)
console.log("item:::",item)
if (item.success) {
if (options != null) {
Modal.success(options);
} else {
createMessage.warning(item.message);
}
} else {
if (options && options.title) {
Modal.warning(options);
} else {
createMessage.warning({
content: '同步失败,请检查对接信息录入中是否填写正确,并确认是否已开启企业微信配置!',
duration: 5,
});
}
}
}
/**
* 查看已绑定的企业微信
*/
function seeBindWeChat() {
openBindModal(true,{ izBind: true })
}
onMounted(() => {
let tenantId = getTenantId();
initThirdAppConfigData({ tenantId: tenantId, thirdType: 'wechat_enterprise' });
});
return {
appConfigData,
weEnterpriseEditClick,
registerAppConfigModal,
registerBindAppConfigModal,
handleSuccess,
btnLoading,
thirdUserByWechat,
handleBindSuccess,
seeBindWeChat,
};
},
});
</script>
<style lang="less" scoped>
.header {
align-items: center;
box-sizing: border-box;
display: flex;
height: 50px;
justify-content: space-between;
font-weight: 700;
font-size: 18px;
color: @text-color;
}
.flex-flow {
display: flex;
min-height: 0;
}
.sync-padding {
padding: 12px 0 16px;
color: @text-color;
}
.base-collapse {
margin-top: 20px;
padding: 0 24px;
font-size: 20px;
.base-desc {
font-size: 14px;
color: @text-color;
}
.base-title {
width: 100px;
text-align: left;
height: 50px;
line-height: 50px;
}
.base-message {
width: 100%;
height: 50px;
line-height: 50px;
}
:deep(.ant-collapse-header) {
padding: 12px 0 16px;
}
:deep(.ant-collapse-content-box) {
padding-left: 0;
}
}
/*begin 兼容暗夜模式*/
//暗黑模式下卡片的边框设置成none
[data-theme='dark'] .base-collapse .ant-collapse {
border: none !important;
}
/*end 兼容暗夜模式*/
/*文档按钮问号样式*/
.sync-text {
margin-left: 2px;
cursor: pointer;
position: relative;
top: 2px;
}
</style>

View File

@ -0,0 +1,78 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/category/rootList',
save = '/sys/category/add',
edit = '/sys/category/edit',
deleteCategory = '/sys/category/delete',
deleteBatch = '/sys/category/deleteBatch',
importExcel = '/sys/category/importExcel',
exportXls = '/sys/category/exportXls',
loadTreeData = '/sys/category/loadTreeRoot',
getChildList = '/sys/category/childList',
getChildListBatch = '/sys/category/getChildListBatch',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
* @param params
*/
export const getImportUrl = Api.importExcel;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除
*/
export const deleteCategory = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteCategory, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除
* @param params
*/
export const batchDeleteCategory = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdateDict = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 查询全部树形节点数据
* @param params
*/
export const loadTreeData = (params) => defHttp.get({ url: Api.loadTreeData, params });
/**
* 查询子节点数据
* @param params
*/
export const getChildList = (params) => defHttp.get({ url: Api.getChildList, params });
/**
* 批量查询子节点数据
* @param params
*/
export const getChildListBatch = (params) => defHttp.get({ url: Api.getChildListBatch, params }, { isTransformResponse: false });

View File

@ -0,0 +1,67 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '分类名称',
dataIndex: 'name',
width: 350,
align: 'left',
},
{
title: '分类编码',
dataIndex: 'code',
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '名称',
field: 'name',
component: 'Input',
colProps: { span: 6 },
},
{
label: '编码',
field: 'code',
component: 'Input',
colProps: { span: 6 },
},
];
export const formSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
label: '父级节点',
field: 'pid',
component: 'TreeSelect',
componentProps: {
//update-begin---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
fieldNames: {
//update-end---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
value: 'key',
},
dropdownStyle: {
maxHeight: '50vh',
},
getPopupContainer: () => document.body,
},
show: ({ values }) => {
return values.pid !== '0';
},
dynamicDisabled: ({ values }) => {
return !!values.id;
},
},
{
label: '分类名称',
field: 'name',
required: true,
component: 'Input',
},
];

View File

@ -0,0 +1,107 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose width="550px" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { BasicForm, useForm } from '/src/components/Form';
import { formSchema } from '../category.data';
import { loadTreeData, saveOrUpdateDict } from '../category.api';
// 获取emit
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const expandedRowKeys = ref([]);
const treeData = ref([]);
const isSubAdd = ref(false);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 18 },
},
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
expandedRowKeys.value = [];
setModalProps({ confirmLoading: false, minHeight: 80 });
isUpdate.value = !!data?.isUpdate;
//update-begin---author:wangshuai ---date: 20230829 for分类字典data.record为空报错------------
isSubAdd.value = !data?.isUpdate && data.record && data.record.id;
//update-end---author:wangshuai ---date: 20230829 for分类字典data.record为空报错------------
if (data?.record) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
//父级节点树信息
treeData.value = await loadTreeData({ async: false, pcode: '' });
updateSchema({
field: 'pid',
componentProps: { treeData },
});
});
//设置标题
const getTitle = computed(() => (!unref(isUpdate) ? '新增字典' : '编辑字典'));
/**
* 根据pid获取展开的节点
* @param pid
* @param arr
*/
function getExpandKeysByPid(pid, arr) {
if (pid && arr && arr.length > 0) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].key == pid && unref(expandedRowKeys).indexOf(pid) < 0) {
//需要获取同一级的key
getSameLevelExpandKeysByPid(arr[i]);
expandedRowKeys.value.push(arr[i].key);
getExpandKeysByPid(arr[i]['parentId'], unref(treeData));
} else {
getExpandKeysByPid(pid, arr[i].children);
}
}
}
}
//表单提交事件
async function handleSubmit() {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateDict(values, isUpdate.value);
//关闭弹窗
closeModal();
//展开的节点信息
await getExpandKeysByPid(values['pid'], unref(treeData));
//刷新列表(isUpdate:是否编辑;values:表单信息;expandedArr:展开的节点信息)
emit('success', { isUpdate: unref(isUpdate), isSubAdd:unref(isSubAdd), values: { ...values }, expandedArr: unref(expandedRowKeys).reverse() });
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 获取同一级的id和同一级的子级id
*/
function getSameLevelExpandKeysByPid(arr) {
if (arr.children && arr.children.length > 0) {
for (const children of arr.children) {
if (unref(expandedRowKeys).indexOf(children.key) < 0 && children.children && children.children.length > 0) {
getSameLevelExpandKeysByPid(children);
expandedRowKeys.value.push(children.key);
}
}
}
}
</script>

View File

@ -0,0 +1,298 @@
<template>
<div>
<!--引用表格-->
<BasicTable
@register="registerTable"
:rowSelection="rowSelection"
:expandedRowKeys="expandedRowKeys"
@expand="handleExpand"
@fetch-success="onFetchSuccess"
>
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<!--字典弹窗-->
<CategoryModal @register="registerModal" @success="handleSuccess" />
</div>
</template>
<script lang="ts" name="system-category" setup>
//ts语法
import { ref, computed, unref, toRaw, nextTick } from 'vue';
import { BasicTable, useTable, TableAction } from '/src/components/Table';
import { useDrawer } from '/src/components/Drawer';
import CategoryModal from './components/CategoryModal.vue';
import { useModal } from '/src/components/Modal';
import { useMethods } from '/src/hooks/system/useMethods';
import { columns, searchFormSchema } from './category.data';
import { list, deleteCategory, batchDeleteCategory, getExportUrl, getImportUrl, getChildList, getChildListBatch } from './category.api';
import { useListPage } from '/@/hooks/system/useListPage';
const expandedRowKeys = ref([]);
const { handleExportXls, handleImportXls } = useMethods();
//字典model
const [registerModal, { openModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, onExportXls, onImportXls, tableContext } = useListPage({
designScope: 'category-template',
tableProps: {
title: '分类字典',
api: list,
columns: columns,
actionColumn: {
width: 180,
},
formConfig: {
schemas: searchFormSchema,
},
isTreeTable: true,
},
exportConfig: {
name: '分类字典列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
//注册table数据
const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] =
tableContext;
/**
* 新增事件
*/
function handleCreate() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
async function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 详情
*/
async function handleDetail(record) {
openModal(true, {
record,
isUpdate: true,
hideFooter: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteCategory({ id: record.id }, importSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
const ids = selectedRowKeys.value.filter((item) => !item.includes('loading'));
await batchDeleteCategory({ ids: ids }, importSuccess);
}
/**
* 导入
*/
function importSuccess() {
//update-begin---author:wangshuai ---date:20220530 for[issues/54]树字典,勾选,然后批量删除,系统错误------------
(selectedRowKeys.value = []) && reload();
//update-end---author:wangshuai ---date:20220530 for[issues/54]树字典,勾选,然后批量删除,系统错误--------------
}
/**
* 添加下级
*/
function handleAddSub(record) {
openModal(true, {
record,
isUpdate: false,
});
}
/**
* 成功回调
*/
async function handleSuccess({ isUpdate,isSubAdd, values, expandedArr }) {
if (isUpdate) {
//编辑回调
updateTableDataRecord(values.id, values);
} else {
if (!values['pid']) {
//新增根节点
reload();
} else {
//新增子集
//update-begin-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长---
if(isSubAdd){
await expandTreeNode(values.pid);
//update-end-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长---
}else{
//update-begin-author:wangshuai---date:20240319--for: 字典树删除之后其他节点出现loading---
//expandedRowKeys.value = [];
//update-end-author:wangshuai---date:20240319--for: 字典树删除之后其他节点出现loading---
for (let key of unref(expandedArr)) {
await expandTreeNode(key);
}
}
}
}
}
/**
* 接口请求成功后回调
*/
function onFetchSuccess(result) {
getDataByResult(result.items) && loadDataByExpandedRows();
}
/**
* 根据已展开的行查询数据(用于保存后刷新时异步加载子级的数据)
*/
async function loadDataByExpandedRows() {
if (unref(expandedRowKeys).length > 0) {
const res = await getChildListBatch({ parentIds: unref(expandedRowKeys).join(',') });
if (res.success && res.result.records.length > 0) {
//已展开的数据批量子节点
let records = res.result.records;
const listMap = new Map();
for (let item of records) {
let pid = item['pid'];
if (unref(expandedRowKeys).includes(pid)) {
let mapList = listMap.get(pid);
if (mapList == null) {
mapList = [];
}
mapList.push(item);
listMap.set(pid, mapList);
}
}
let childrenMap = listMap;
let fn = (list) => {
if (list) {
list.forEach((data) => {
if (unref(expandedRowKeys).includes(data.id)) {
data.children = getDataByResult(childrenMap.get(data.id));
fn(data.children);
}
});
}
};
fn(getDataSource());
}
}
}
/**
* 处理数据集
*/
function getDataByResult(result) {
if (result && result.length > 0) {
return result.map((item) => {
//判断是否标记了带有子节点
if (item['hasChild'] == '1') {
let loadChild = { id: item.id + '_loadChild', name: 'loading...', isLoading: true };
item.children = [loadChild];
}
return item;
});
}
}
/**
*树节点展开合并
* */
async function handleExpand(expanded, record) {
// 判断是否是展开状态,展开状态(expanded)并且存在子集(children)并且未加载过(isLoading)的就去查询子节点数据
if (expanded) {
expandedRowKeys.value.push(record.id);
if (record.children.length > 0 && !!record.children[0].isLoading) {
let result = await getChildList({ pid: record.id });
if (result && result.length > 0) {
record.children = getDataByResult(result);
} else {
record.children = null;
record.hasChild = '0';
}
}
} else {
let keyIndex = expandedRowKeys.value.indexOf(record.id);
if (keyIndex >= 0) {
expandedRowKeys.value.splice(keyIndex, 1);
}
}
}
/**
*操作表格后处理树节点展开合并
* */
async function expandTreeNode(key) {
let record:any = findTableDataRecord(key);
//update-begin-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长,显示“接口请求超时,请刷新页面重试!”---
if(!expandedRowKeys.value.includes(key)){
expandedRowKeys.value.push(key);
}
//update-end-author:liusq---date:20230411--for: [issue/4550]分类字典数据量过多会造成数据查询时间过长,显示“接口请求超时,请刷新页面重试!”---
let result = await getChildList({ pid: key });
if (result && result.length > 0) {
record.children = getDataByResult(result);
} else {
record.children = null;
record.hasChild = '0';
}
updateTableDataRecord(key, record);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '确定删除吗?',
confirm: handleDelete.bind(null, record),
},
},
{
label: '添加下级',
onClick: handleAddSub.bind(null, { pid: record.id }),
},
];
}
</script>
<style scoped></style>

View File

@ -0,0 +1,247 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" @ok="handleSubmit" :title="title" :width="1200" destroyOnClose>
<BasicForm @register="registerForm" />
<a-tabs v-model:activeKey="activeKey" animated>
<a-tab-pane tab="局部规则" key="1" :forceRender="true">
<JVxeTable ref="vTable1" toolbar rowNumber dragSort rowSelection :maxHeight="580" :dataSource="dataSource1" :columns="columns1">
<template #toolbarAfter>
<a-alert type="info" showIcon message="局部规则按照你输入的位数有序的校验" style="margin-bottom: 8px" />
</template>
</JVxeTable>
</a-tab-pane>
<a-tab-pane tab="全局规则" key="2" :forceRender="true">
<JVxeTable
ref="vTable2"
toolbar
rowNumber
dragSort
rowSelection
:maxHeight="580"
:dataSource="dataSource2"
:addSetActive="false"
:columns="columns2"
>
<template #toolbarAfter>
<a-alert type="info" showIcon message="全局规则可校验用户输入的所有字符;全局规则的优先级比局部规则的要高。" style="margin-bottom: 8px" />
</template>
</JVxeTable>
</a-tab-pane>
</a-tabs>
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { computed, ref, unref } from 'vue';
import { formSchema } from './check.rule.data';
import { saveCheckRule, updateCheckRule } from './check.rule.api';
import { JVxeTypes, JVxeColumn, JVxeTableInstance } from '/@/components/jeecg/JVxeTable/types';
import { pick } from 'lodash-es';
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));
// 声明Emits
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate, getFieldsValue }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
});
const activeKey = ref('1');
let arr1: any[] = [];
let dataSource1 = ref(arr1);
let arr2: any[] = [];
let dataSource2 = ref(arr2);
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
activeKey.value = '1';
dataSource1.value = [];
dataSource2.value = [];
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
let ruleJson = data.record.ruleJson;
if (ruleJson) {
let ruleList = JSON.parse(ruleJson);
// 筛选出全局规则和局部规则
let global: any[] = [],
design: any[] = [],
priority = '1';
ruleList.forEach((rule) => {
if (rule.digits === '*') {
global.push(Object.assign(rule, { priority }));
} else {
priority = '0';
design.push(rule);
}
});
dataSource1.value = design;
dataSource2.value = global;
}
}
});
const vTable1 = ref<JVxeTableInstance>();
const vTable2 = ref<JVxeTableInstance>();
// 验证表格 返回表格数据
function validateMyTable(tableRef, key) {
return new Promise((resolve, reject) => {
tableRef.value!.validateTable().then((errMap) => {
if (errMap) {
activeKey.value = key;
reject();
} else {
const values = tableRef.value!.getTableData();
resolve(values);
}
});
});
}
//表单提交事件
async function handleSubmit() {
let mainData;
let globalValues = [];
let designValues = [];
validate()
.then((formValue) => {
mainData = formValue;
return validateMyTable(vTable1, '1');
})
.then((tableData1: []) => {
if (tableData1 && tableData1.length > 0) {
designValues = tableData1;
}
return validateMyTable(vTable2, '2');
})
.then((tableData2: []) => {
if (tableData2 && tableData2.length > 0) {
globalValues = tableData2;
}
// 整合两个子表的数据
let firstGlobal: any[] = [],
afterGlobal: any[] = [];
for (let i = 0; i < globalValues.length; i++) {
let v: any = globalValues[i];
v.digits = '*';
if (v.priority === '1') {
firstGlobal.push(v);
} else {
afterGlobal.push(v);
}
}
let concatValues = firstGlobal.concat(designValues).concat(afterGlobal);
let subValues = concatValues.map((i) => pick(i, 'digits', 'pattern', 'message'));
// 生成 formData用于传入后台
let ruleJson = JSON.stringify(subValues);
let formData = Object.assign({}, mainData, { ruleJson });
saveOrUpdateFormData(formData);
})
.catch(() => {
setModalProps({ confirmLoading: false });
console.error('验证未通过!');
});
}
// 表单提交请求
async function saveOrUpdateFormData(formData) {
try {
console.log('表单提交数据', formData);
setModalProps({ confirmLoading: true });
if (isUpdate.value) {
await updateCheckRule(formData);
} else {
await saveCheckRule(formData);
}
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 校验
* @param cellValue
* @param callback
*/
const validatePatternHandler = ({ cellValue }, callback) => {
try {
new RegExp(cellValue);
callback(true);
} catch (e) {
callback(false, '请输入正确的正则表达式');
}
};
const columns1 = ref<JVxeColumn[]>([
{
title: '位数',
key: 'digits',
type: JVxeTypes.inputNumber,
minWidth: 180,
validateRules: [
{ required: true, message: '${title}不能为空' },
{ pattern: /^[1-9]\d*$/, message: '请输入零以上的正整数' },
],
},
{
title: '规则正则表达式',
key: 'pattern',
minWidth: 320,
type: JVxeTypes.input,
validateRules: [{ required: true, message: '规则不能为空' }, { handler: validatePatternHandler }],
},
{
title: '提示文本',
key: 'message',
minWidth: 180,
type: JVxeTypes.input,
validateRules: [{ required: true, message: '${title}不能为空' }],
},
]);
const columns2 = ref<JVxeColumn[]>([
{
title: '优先级',
key: 'priority',
type: JVxeTypes.select,
defaultValue: '1',
options: [
{ title: '优先运行', value: '1' },
{ title: '最后运行', value: '0' },
],
validateRules: [],
},
{
title: '规则正则表达式',
key: 'pattern',
width: '40%',
type: JVxeTypes.input,
validateRules: [{ required: true, message: '规则不能为空' }, { handler: validatePatternHandler }],
},
{
title: '提示文本',
key: 'message',
width: '20%',
type: JVxeTypes.input,
validateRules: [{ required: true, message: '${title}不能为空' }],
},
]);
</script>

View File

@ -0,0 +1,55 @@
<template>
<BasicModal v-bind="$attrs" :okButtonProps="okButtonProps" @register="registerModal" destroyOnClose>
<BasicForm @register="registerForm" />
<div style="display: flex; flex-flow: row wrap">
<div style="padding: 0 4px" v-for="(str, index) of realTestValue" :key="index">
<a-row>
<a-col style="text-align: center">
<a-input :value="str" style="text-align: center; width: 40px" />
</a-col>
<a-col style="text-align: center">{{ index + 1 }}</a-col>
</a-row>
</div>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { checkRuleInput } from '/@/views/system/checkRule/check.rule.data';
import { ref } from 'vue';
let realTestValue = ref('');
const okButtonProps = {
style: { display: 'none' },
};
const [registerForm, { resetFields, setFieldsValue, validate, getFieldsValue }] = useForm({
schemas: checkRuleInput,
showActionButtonGroup: false,
labelCol: {
span: 24,
},
wrapperCol: {
span: 24,
},
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
realTestValue.value = '';
setModalProps({
confirmLoading: false,
cancelText: '关闭',
title: '功能测试',
width: '1000px',
});
await setFieldsValue({
ruleCode: data.ruleCode,
testValue: realTestValue,
});
});
</script>
<style scoped></style>

View File

@ -0,0 +1,86 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/checkRule/list',
delete = '/sys/checkRule/delete',
deleteBatch = '/sys/checkRule/deleteBatch',
exportXls = 'sys/checkRule/exportXls',
importXls = 'sys/checkRule/importExcel',
checkByCode = '/sys/checkRule/checkByCode',
save = '/sys/checkRule/add',
edit = '/sys/checkRule/edit',
}
/**
* 导出地址
*/
export const exportUrl = Api.exportXls;
/**
* 导入地址
*/
export const importUrl = Api.importXls;
/**
* 列表查询
* @param params
*/
export const getCheckRuleList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 删除
* @param params
* @param handleSuccess
*/
export const deleteCheckRule = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除
* @param params
*/
export const batchDeleteCheckRule = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 根据编码校验规则code校验传入的值是否合法
* @param ruleCode
* @param value
*/
export const validateCheckRule = (ruleCode, value) => {
value = encodeURIComponent(value);
let params = { ruleCode, value };
return defHttp.get({ url: Api.checkByCode, params }, { isTransformResponse: false });
};
/**
* 保存
* @param params
*/
export const saveCheckRule = (params) => {
return defHttp.post({ url: Api.save, params });
};
/**
* 更新
* @param params
*/
export const updateCheckRule = (params) => {
return defHttp.put({ url: Api.edit, params });
};

View File

@ -0,0 +1,152 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils';
import { duplicateCheckDelay } from '/@/views/system/user/user.api';
import { validateCheckRule } from '/@/views/system/checkRule/check.rule.api';
import { array } from 'vue-types';
export const columns: BasicColumn[] = [
{
title: '规则名称',
dataIndex: 'ruleName',
width: 200,
align: 'center',
},
{
title: '规则编码',
dataIndex: 'ruleCode',
width: 200,
align: 'center',
},
{
title: '规则描述',
dataIndex: 'ruleDescription',
width: 300,
align: 'center',
customRender: function ({ text }) {
return render.renderTip(text, 30);
},
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'ruleName',
label: '规则名称',
component: 'Input',
colProps: { span: 6 },
},
{
field: 'ruleCode',
label: '规则编码',
component: 'Input',
colProps: { span: 6 },
},
];
export const formSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
field: 'ruleName',
label: '规则名称',
component: 'Input',
required: true,
colProps: { span: 24 },
},
{
field: 'ruleCode',
label: '规则编码',
component: 'Input',
colProps: { span: 24 },
dynamicDisabled: ({ values }) => {
return !!values.id;
},
dynamicRules: ({ model }) => {
return [
{
required: true,
validator: (_, value) => {
return new Promise((resolve, reject) => {
if (!value) {
return reject('请输入规则编码!');
}
let params = {
tableName: 'sys_check_rule',
fieldName: 'rule_code',
fieldVal: value,
dataId: model.id,
};
duplicateCheckDelay(params)
.then((res) => {
res.success ? resolve() : reject('规则编码已存在!');
})
.catch((err) => {
reject(err.message || '校验失败');
});
});
},
},
];
},
},
{
field: 'ruleDescription',
label: '规则描述',
colProps: { span: 24 },
component: 'InputTextArea',
componentProps: {
placeholder: '请输入规则描述',
rows: 2,
},
},
];
export const checkRuleInput: FormSchema[] = [
{
label: '123',
field: 'ruleCode',
component: 'Input',
show: false,
},
{
field: 'testValue',
label: '需要测试的值:',
component: 'Input',
componentProps: ({ formModel }) => {
return {
onChange: (e) => {
formModel.testValue = e.target.value;
},
};
},
dynamicRules: ({ model }) => {
const { ruleCode } = model;
return [
{
required: false,
validator: (_, value) => {
return new Promise((resolve, reject) => {
if (ruleCode && value) {
/*console.log({ruleCode,value})*/
validateCheckRule(ruleCode, value)
.then((res) => {
//console.log(1233, res)
res['success'] ? resolve() : reject(res['message']);
})
.catch((err) => {
reject(err.message || err);
});
} else {
resolve();
}
});
},
},
];
},
},
];

View File

@ -0,0 +1,150 @@
<template>
<div :class="prefixCls">
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作</span>
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<CheckRuleTestModal @register="testModal"></CheckRuleTestModal>
<CheckRuleModal @register="registerModal" @success="reload"></CheckRuleModal>
</div>
</template>
<script name="system-fillrule" lang="ts" setup>
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { useModal } from '/@/components/Modal';
import {
getCheckRuleList,
exportUrl,
importUrl,
deleteCheckRule,
batchDeleteCheckRule,
validateCheckRule,
} from '/@/views/system/checkRule/check.rule.api';
import { columns, searchFormSchema } from '/@/views/system/checkRule/check.rule.data';
import { ActionItem } from '/@/components/Table';
import CheckRuleTestModal from '/@/views/system/checkRule/CheckRuleTestModal.vue';
const [testModal, { openModal: openTestModal }] = useModal();
import CheckRuleModal from '/@/views/system/checkRule/CheckRuleModal.vue';
const [registerModal, { openModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, tableContext, createMessage, onExportXls, onImportXls, createSuccessModal } = useListPage({
designScope: 'check-rule',
tableProps: {
title: '编码校验规则管理页面',
api: getCheckRuleList,
columns: columns,
showIndexColumn: true,
formConfig: {
schemas: searchFormSchema,
},
},
exportConfig: {
url: exportUrl,
name: '编码校验规则列表',
},
importConfig: {
url: importUrl,
success: () => reload(),
},
});
// 注册 ListTable
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
console.log('record....', record);
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
console.log(12345, record);
await deleteCheckRule({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteCheckRule({ ids: selectedRowKeys.value }, () => {
selectedRowKeys.value = [];
reload();
});
}
/**
* 功能测试
*/
function testRule(record) {
openTestModal(true, { ruleCode: record.ruleCode });
}
/**
* 编辑
*/
function getTableAction(record): ActionItem[] {
return [{ label: '编辑', onClick: handleEdit.bind(null, record) }];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record): ActionItem[] {
return [
{ label: '功能测试', onClick: testRule.bind(null, record) },
{
label: '删除',
color: 'error',
popConfirm: {
title: '确认要删除吗?',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>

View File

@ -0,0 +1,78 @@
<template>
<BasicDrawer title="数据规则/按钮权限配置" :width="365" @close="onClose" @register="registerDrawer">
<a-spin :spinning="loading">
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="数据规则" key="1">
<a-checkbox-group v-model:value="dataRuleChecked" v-if="dataRuleList.length > 0">
<a-row>
<a-col :span="24" v-for="(item, index) in dataRuleList" :key="'dr' + index">
<a-checkbox :value="item.id">{{ item.ruleName }}</a-checkbox>
</a-col>
<a-col :span="24">
<div style="width: 100%; margin-top: 15px">
<a-button type="primary" :loading="loading" :size="'small'" preIcon="ant-design:save-filled" @click="saveDataRuleForRole">
<span>点击保存</span>
</a-button>
</div>
</a-col>
</a-row>
</a-checkbox-group>
<a-empty v-else description="无配置信息" />
</a-tab-pane>
</a-tabs>
</a-spin>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { queryDepartDataRule, saveDepartDataRule } from '../depart.api';
defineEmits(['register']);
const loading = ref<boolean>(false);
const departId = ref('');
const functionId = ref('');
const dataRuleList = ref<Array<any>>([]);
const dataRuleChecked = ref<Array<any>>([]);
// 注册抽屉组件
const [registerDrawer, { closeDrawer }] = useDrawerInner((data) => {
departId.value = unref(data.departId);
functionId.value = unref(data.functionId);
loadData();
});
async function loadData() {
try {
loading.value = true;
const { datarule, drChecked } = await queryDepartDataRule(functionId, departId);
dataRuleList.value = datarule;
if (drChecked) {
dataRuleChecked.value = drChecked.split(',');
}
} finally {
loading.value = false;
}
}
function saveDataRuleForRole() {
let params = {
departId: departId.value,
permissionId: functionId.value,
dataRuleIds: dataRuleChecked.value.join(','),
};
saveDepartDataRule(params);
}
function onClose() {
doReset();
}
function doReset() {
functionId.value = '';
dataRuleList.value = [];
dataRuleChecked.value = [];
}
</script>

View File

@ -0,0 +1,92 @@
<template>
<BasicModal :title="title" :width="800" v-bind="$attrs" @ok="handleOk" @register="registerModal">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { watch, computed, inject, ref, unref, onMounted } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { saveOrUpdateDepart } from '../depart.api';
import { useBasicFormSchema, orgCategoryOptions } from '../depart.data';
const emit = defineEmits(['success', 'register']);
const props = defineProps({
rootTreeData: { type: Array, default: () => [] },
});
const prefixCls = inject('prefixCls');
// 当前是否是更新模式
const isUpdate = ref<boolean>(false);
// 当前的弹窗数据
const model = ref<object>({});
const title = computed(() => (isUpdate.value ? '编辑' : '新增'));
//注册表单
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: useBasicFormSchema().basicFormSchema,
showActionButtonGroup: false,
});
// 注册弹窗
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
isUpdate.value = unref(data?.isUpdate);
// 当前是否为添加子级
let isChild = unref(data?.isChild);
let categoryOptions = isChild ? orgCategoryOptions.child : orgCategoryOptions.root;
// 隐藏不需要展示的字段
updateSchema([
{
field: 'parentId',
show: isChild,
componentProps: {
// 如果是添加子部门,就禁用该字段
disabled: isChild,
treeData: props.rootTreeData,
},
},
{
field: 'orgCode',
show: false,
},
{
field: 'orgCategory',
componentProps: { options: categoryOptions },
},
]);
let record = unref(data?.record);
if (typeof record !== 'object') {
record = {};
}
// 赋默认值
record = Object.assign(
{
departOrder: 0,
orgCategory: categoryOptions[0].value,
},
record
);
model.value = record;
await setFieldsValue({ ...record });
});
// 提交事件
async function handleOk() {
try {
setModalProps({ confirmLoading: true });
let values = await validate();
//提交表单
await saveOrUpdateDepart(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,128 @@
<template>
<a-spin :spinning="loading">
<BasicForm @register="registerForm" />
<div class="j-box-bottom-button offset-20" style="margin-top: 30px">
<div class="j-box-bottom-button-float" :class="[`${prefixCls}`]">
<a-button preIcon="ant-design:sync-outlined" @click="onReset">重置</a-button>
<a-button type="primary" preIcon="ant-design:save-filled" @click="onSubmit">保存</a-button>
</div>
</div>
</a-spin>
</template>
<script lang="ts" setup>
import { watch, computed, inject, ref, unref, onMounted } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { saveOrUpdateDepart } from '../depart.api';
import { useBasicFormSchema, orgCategoryOptions } from '../depart.data';
import { useDesign } from '/@/hooks/web/useDesign';
const { prefixCls } = useDesign('j-depart-form-content');
const emit = defineEmits(['success']);
const props = defineProps({
data: { type: Object, default: () => ({}) },
rootTreeData: { type: Array, default: () => [] },
});
const loading = ref<boolean>(false);
// 当前是否是更新模式
const isUpdate = ref<boolean>(true);
// 当前的弹窗数据
const model = ref<object>({});
//注册表单
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: useBasicFormSchema().basicFormSchema,
showActionButtonGroup: false,
});
const categoryOptions = computed(() => {
if (!!props?.data?.parentId) {
return orgCategoryOptions.child;
} else {
return orgCategoryOptions.root;
}
});
onMounted(() => {
// 禁用字段
updateSchema([
{ field: 'parentId', componentProps: { disabled: true } },
{ field: 'orgCode', componentProps: { disabled: true } },
]);
// data 变化,重填表单
watch(
() => props.data,
async () => {
let record = unref(props.data);
if (typeof record !== 'object') {
record = {};
}
model.value = record;
await resetFields();
await setFieldsValue({ ...record });
},
{ deep: true, immediate: true }
);
// 更新 父部门 选项
watch(
() => props.rootTreeData,
async () => {
updateSchema([
{
field: 'parentId',
componentProps: { treeData: props.rootTreeData },
},
]);
},
{ deep: true, immediate: true }
);
// 监听并更改 orgCategory options
watch(
categoryOptions,
async () => {
updateSchema([
{
field: 'orgCategory',
componentProps: { options: categoryOptions.value },
},
]);
},
{ immediate: true }
);
});
// 重置表单
async function onReset() {
await resetFields();
await setFieldsValue({ ...model.value });
}
// 提交事件
async function onSubmit() {
try {
loading.value = true;
let values = await validate();
values = Object.assign({}, model.value, values);
//提交表单
await saveOrUpdateDepart(values, isUpdate.value);
//刷新列表
emit('success');
Object.assign(model.value, values);
} finally {
loading.value = false;
}
}
</script>
<style lang="less">
// update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
@prefix-cls: ~'@{namespace}-j-depart-form-content';
/*begin 兼容暗夜模式*/
.@{prefix-cls} {
background: @component-background;
border-top: 1px solid @border-color-base;
}
/*end 兼容暗夜模式*/
// update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
</style>

View File

@ -0,0 +1,332 @@
<template>
<a-card :bordered="false" style="height: 100%">
<div class="j-table-operator" style="width: 100%">
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAddDepart">新增</a-button>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAddChildDepart()">添加下级</a-button>
<a-upload name="file" :showUploadList="false" :customRequest="onImportXls">
<a-button type="primary" preIcon="ant-design:import-outlined">导入</a-button>
</a-upload>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<a-button type="primary" preIcon="ant-design:sync-outlined">同步企微?</a-button>
<a-button type="primary" preIcon="ant-design:sync-outlined">同步钉钉?</a-button>
<template v-if="checkedKeys.length > 0">
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="onDeleteBatch">
<icon icon="ant-design:delete-outlined" />
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作 </span>
<icon icon="akar-icons:chevron-down" />
</a-button>
</a-dropdown>
</template>
</div>
<a-alert type="info" show-icon class="alert" style="margin-bottom: 8px">
<template #message>
<template v-if="checkedKeys.length > 0">
<span>已选中 {{ checkedKeys.length }} 条记录</span>
<a-divider type="vertical" />
<a @click="checkedKeys = []">清空</a>
</template>
<template v-else>
<span>未选中任何数据</span>
</template>
</template>
</a-alert>
<a-spin :spinning="loading">
<a-input-search placeholder="按部门名称搜索" style="margin-bottom: 10px" @search="onSearch" />
<!--组织机构树-->
<template v-if="treeData.length > 0">
<a-tree
v-if="!treeReloading"
checkable
:clickRowToExpand="false"
:treeData="treeData"
:selectedKeys="selectedKeys"
:checkStrictly="checkStrictly"
:load-data="loadChildrenTreeData"
:checkedKeys="checkedKeys"
v-model:expandedKeys="expandedKeys"
@check="onCheck"
@select="onSelect"
>
<template #title="{ key: treeKey, title, dataRef }">
<a-dropdown :trigger="['contextmenu']">
<Popconfirm
:open="visibleTreeKey === treeKey"
title="确定要删除吗"
ok-text="确定"
cancel-text="取消"
placement="rightTop"
@confirm="onDelete(dataRef)"
@openChange="onVisibleChange"
>
<span>{{ title }}</span>
</Popconfirm>
<template #overlay>
<a-menu @click="">
<a-menu-item key="1" @click="onAddChildDepart(dataRef)">添加子级</a-menu-item>
<a-menu-item key="2" @click="visibleTreeKey = treeKey">
<span style="color: red">删除</span>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-tree>
</template>
<a-empty v-else description="暂无数据" />
</a-spin>
<DepartFormModal :rootTreeData="treeData" @register="registerModal" @success="loadRootTreeData" />
</a-card>
</template>
<script lang="ts" setup>
import { inject, nextTick, ref, unref } from 'vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useMethods } from '/@/hooks/system/useMethods';
import { Api, deleteBatchDepart, queryDepartTreeSync } from '../depart.api';
import { searchByKeywords } from '/@/views/system/departUser/depart.user.api';
import DepartFormModal from '/@/views/system/depart/components/DepartFormModal.vue';
import { Popconfirm } from 'ant-design-vue';
const prefixCls = inject('prefixCls');
const emit = defineEmits(['select', 'rootTreeData']);
const { createMessage } = useMessage();
const { handleImportXls, handleExportXls } = useMethods();
const loading = ref<boolean>(false);
// 部门树列表数据
const treeData = ref<any[]>([]);
// 当前选中的项
const checkedKeys = ref<any[]>([]);
// 当前展开的项
const expandedKeys = ref<any[]>([]);
// 当前选中的项
const selectedKeys = ref<any[]>([]);
// 树组件重新加载
const treeReloading = ref<boolean>(false);
// 树父子是否关联
const checkStrictly = ref<boolean>(true);
// 当前选中的部门
const currentDepart = ref<any>(null);
// 控制确认删除提示框是否显示
const visibleTreeKey = ref<any>(null);
// 搜索关键字
const searchKeyword = ref('');
// 注册 modal
const [registerModal, { openModal }] = useModal();
// 加载顶级部门信息
async function loadRootTreeData() {
try {
loading.value = true;
treeData.value = [];
const result = await queryDepartTreeSync();
if (Array.isArray(result)) {
treeData.value = result;
}
if (expandedKeys.value.length === 0) {
autoExpandParentNode();
} else {
if (selectedKeys.value.length === 0) {
let item = treeData.value[0];
if (item) {
// 默认选中第一个
setSelectedKey(item.id, item);
}
} else {
emit('select', currentDepart.value);
}
}
emit('rootTreeData', treeData.value);
} finally {
loading.value = false;
}
}
loadRootTreeData();
// 加载子级部门信息
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
pid: treeNode.dataRef.id,
});
if (result.length == 0) {
treeNode.dataRef.isLeaf = true;
} else {
treeNode.dataRef.children = result;
if (expandedKeys.value.length > 0) {
// 判断获取的子级是否有当前展开的项
let subKeys: any[] = [];
for (let key of expandedKeys.value) {
if (result.findIndex((item) => item.id === key) !== -1) {
subKeys.push(key);
}
}
if (subKeys.length > 0) {
expandedKeys.value = [...expandedKeys.value];
}
}
}
treeData.value = [...treeData.value];
emit('rootTreeData', treeData.value);
} catch (e) {
console.error(e);
}
return Promise.resolve();
}
// 自动展开父节点,只展开一级
function autoExpandParentNode() {
let item = treeData.value[0];
if (item) {
if (!item.isLeaf) {
expandedKeys.value = [item.key];
}
// 默认选中第一个
setSelectedKey(item.id, item);
reloadTree();
} else {
emit('select', null);
}
}
// 重新加载树组件,防止无法默认展开数据
async function reloadTree() {
await nextTick();
treeReloading.value = true;
await nextTick();
treeReloading.value = false;
}
/**
* 设置当前选中的行
*/
function setSelectedKey(key: string, data?: object) {
selectedKeys.value = [key];
if (data) {
currentDepart.value = data;
emit('select', data);
}
}
// 添加一级部门
function onAddDepart() {
openModal(true, { isUpdate: false, isChild: false });
}
// 添加子级部门
function onAddChildDepart(data = currentDepart.value) {
if (data == null) {
createMessage.warning('请先选择一个部门');
return;
}
const record = { parentId: data.id };
openModal(true, { isUpdate: false, isChild: true, record });
}
// 搜索事件
async function onSearch(value: string) {
if (value) {
try {
loading.value = true;
treeData.value = [];
let result = await searchByKeywords({ keyWord: value });
if (Array.isArray(result)) {
treeData.value = result;
}
autoExpandParentNode();
} finally {
loading.value = false;
}
} else {
loadRootTreeData();
}
searchKeyword.value = value;
}
// 树复选框选择事件
function onCheck(e) {
if (Array.isArray(e)) {
checkedKeys.value = e;
} else {
checkedKeys.value = e.checked;
}
}
// 树选择事件
function onSelect(selKeys, event) {
console.log('select: ', selKeys, event);
if (selKeys.length > 0 && selectedKeys.value[0] !== selKeys[0]) {
setSelectedKey(selKeys[0], event.selectedNodes[0]);
} else {
// 这样可以防止用户取消选择
setSelectedKey(selectedKeys.value[0]);
}
}
/**
* 根据 ids 删除部门
* @param idListRef array
* @param confirm 是否显示确认提示框
*/
async function doDeleteDepart(idListRef, confirm = true) {
const idList = unref(idListRef);
if (idList.length > 0) {
try {
loading.value = true;
await deleteBatchDepart({ ids: idList.join(',') }, confirm);
await loadRootTreeData();
} finally {
loading.value = false;
}
}
}
// 删除单个部门
async function onDelete(data) {
if (data) {
onVisibleChange(false);
doDeleteDepart([data.id], false);
}
}
// 批量删除部门
async function onDeleteBatch() {
try {
await doDeleteDepart(checkedKeys);
checkedKeys.value = [];
} finally {
}
}
function onVisibleChange(visible) {
if (!visible) {
visibleTreeKey.value = null;
}
}
function onImportXls(d) {
handleImportXls(d, Api.importExcelUrl, () => {
loadRootTreeData();
});
}
function onExportXls() {
handleExportXls('部门信息', Api.exportXlsUrl);
}
defineExpose({
loadRootTreeData,
});
</script>

View File

@ -0,0 +1,173 @@
<template>
<a-spin :spinning="loading">
<template v-if="treeData.length > 0">
<BasicTree
ref="basicTree"
class="depart-rule-tree"
checkable
:treeData="treeData"
:checkedKeys="checkedKeys"
:selectedKeys="selectedKeys"
:expandedKeys="expandedKeys"
:checkStrictly="checkStrictly"
style="height: 500px; overflow: auto"
@check="onCheck"
@expand="onExpand"
@select="onSelect"
>
<template #title="{ slotTitle, ruleFlag }">
<span>{{ slotTitle }}</span>
<Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red" />
</template>
</BasicTree>
</template>
<a-empty v-else description="无可配置部门权限" />
<div class="j-box-bottom-button offset-20" style="margin-top: 30px">
<div class="j-box-bottom-button-float" :class="[`${prefixCls}`]">
<a-dropdown :trigger="['click']" placement="top">
<template #overlay>
<a-menu>
<a-menu-item key="3" @click="toggleCheckALL(true)">全部勾选</a-menu-item>
<a-menu-item key="4" @click="toggleCheckALL(false)">取消全选</a-menu-item>
<a-menu-item key="5" @click="toggleExpandAll(true)">展开所有</a-menu-item>
<a-menu-item key="6" @click="toggleExpandAll(false)">收起所有</a-menu-item>
</a-menu>
</template>
<a-button style="float: left">
树操作
<Icon icon="ant-design:up-outlined" />
</a-button>
</a-dropdown>
<a-button type="primary" preIcon="ant-design:save-filled" @click="onSubmit">保存</a-button>
</div>
</div>
</a-spin>
<DepartDataRuleDrawer @register="registerDataRuleDrawer" />
</template>
<script lang="ts" setup>
import { watch, computed, inject, ref, nextTick } from 'vue';
import { useDrawer } from '/@/components/Drawer';
import { BasicTree } from '/@/components/Tree/index';
import DepartDataRuleDrawer from './DepartDataRuleDrawer.vue';
import { queryRoleTreeList, queryDepartPermission, saveDepartPermission } from '../depart.api';
import { useDesign } from '/@/hooks/web/useDesign';
import { translateTitle } from '/@/utils/common/compUtils';
const { prefixCls } = useDesign('j-depart-form-content');
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
// 当前选中的部门ID可能会为空代表未选择部门
const departId = computed(() => props.data?.id);
const basicTree = ref();
const loading = ref<boolean>(false);
const treeData = ref<any[]>([]);
const expandedKeys = ref<Array<any>>([]);
const selectedKeys = ref<Array<any>>([]);
const checkedKeys = ref<Array<any>>([]);
const lastCheckedKeys = ref<Array<any>>([]);
const checkStrictly = ref(true);
// 注册数据规则授权弹窗抽屉
const [registerDataRuleDrawer, dataRuleDrawer] = useDrawer();
// onCreated
loadData();
watch(departId, () => loadDepartPermission(), { immediate: true });
async function loadData() {
try {
loading.value = true;
let { treeList } = await queryRoleTreeList();
//update-begin---author:wangshuai---date:2024-04-08---for:【issues/1169】部门管理功能中的【部门权限】中未翻译 t('') 多语言---
treeData.value = translateTitle(treeList);
//update-end---author:wangshuai---date:2024-04-08---for:【issues/1169】部门管理功能中的【部门权限】中未翻译 t('') 多语言---
await nextTick();
toggleExpandAll(true);
} finally {
loading.value = false;
}
}
async function loadDepartPermission() {
if (departId.value) {
try {
loading.value = true;
let keys = await queryDepartPermission({ departId: departId.value });
checkedKeys.value = keys;
lastCheckedKeys.value = [...keys];
} finally {
loading.value = false;
}
}
}
async function onSubmit() {
try {
loading.value = true;
await saveDepartPermission({
departId: departId.value,
permissionIds: checkedKeys.value.join(','),
lastpermissionIds: lastCheckedKeys.value.join(','),
});
await loadData();
await loadDepartPermission();
} finally {
loading.value = false;
}
}
// tree勾选复选框事件
function onCheck(event) {
if (!Array.isArray(event)) {
checkedKeys.value = event.checked;
} else {
checkedKeys.value = event;
}
}
// tree展开事件
function onExpand($expandedKeys) {
expandedKeys.value = $expandedKeys;
}
// tree选中事件
function onSelect($selectedKeys, { selectedNodes }) {
if (selectedNodes[0]?.ruleFlag) {
let functionId = $selectedKeys[0];
dataRuleDrawer.openDrawer(true, { departId, functionId });
}
selectedKeys.value = [];
}
// 切换父子关联
async function toggleCheckStrictly(flag) {
checkStrictly.value = flag;
await nextTick();
checkedKeys.value = basicTree.value.getCheckedKeys();
}
// 切换展开收起
async function toggleExpandAll(flag) {
basicTree.value.expandAll(flag);
await nextTick();
expandedKeys.value = basicTree.value.getExpandedKeys();
}
// 切换全选
async function toggleCheckALL(flag) {
basicTree.value.checkAll(flag);
await nextTick();
checkedKeys.value = basicTree.value.getCheckedKeys();
}
</script>
<style lang="less" scoped>
// 【VUEN-188】解决滚动条不灵敏的问题
.depart-rule-tree :deep(.scrollbar__bar) {
pointer-events: none;
}
</style>

View File

@ -0,0 +1,122 @@
import { unref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
export enum Api {
queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync',
save = '/sys/sysDepart/add',
edit = '/sys/sysDepart/edit',
delete = '/sys/sysDepart/delete',
deleteBatch = '/sys/sysDepart/deleteBatch',
exportXlsUrl = '/sys/sysDepart/exportXls',
importExcelUrl = '/sys/sysDepart/importExcel',
roleQueryTreeList = '/sys/role/queryTreeList',
queryDepartPermission = '/sys/permission/queryDepartPermission',
saveDepartPermission = '/sys/permission/saveDepartPermission',
dataRule = '/sys/sysDepartPermission/datarule',
getCurrentUserDeparts = '/sys/user/getCurrentUserDeparts',
selectDepart = '/sys/selectDepart',
getUpdateDepartInfo = '/sys/user/getUpdateDepartInfo',
doUpdateDepartInfo = '/sys/user/doUpdateDepartInfo',
changeDepartChargePerson = '/sys/user/changeDepartChargePerson',
}
/**
* 获取部门树列表
*/
export const queryDepartTreeSync = (params?) => defHttp.get({ url: Api.queryDepartTreeSync, params });
/**
* 保存或者更新部门角色
*/
export const saveOrUpdateDepart = (params, isUpdate) => {
if (isUpdate) {
return defHttp.put({ url: Api.edit, params });
} else {
return defHttp.post({ url: Api.save, params });
}
};
/**
* 批量删除部门角色
*/
export const deleteBatchDepart = (params, confirm = false) => {
return new Promise((resolve, reject) => {
const doDelete = () => {
resolve(defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }));
};
if (confirm) {
createConfirm({
iconType: 'warning',
title: '删除',
content: '确定要删除吗?',
onOk: () => doDelete(),
onCancel: () => reject(),
});
} else {
doDelete();
}
});
};
/**
* 获取权限树列表
*/
export const queryRoleTreeList = (params?) => defHttp.get({ url: Api.roleQueryTreeList, params });
/**
* 查询部门权限
*/
export const queryDepartPermission = (params?) => defHttp.get({ url: Api.queryDepartPermission, params });
/**
* 保存部门权限
*/
export const saveDepartPermission = (params) => defHttp.post({ url: Api.saveDepartPermission, params });
/**
* 查询部门数据权限列表
*/
export const queryDepartDataRule = (functionId, departId, params?) => {
let url = `${Api.dataRule}/${unref(functionId)}/${unref(departId)}`;
return defHttp.get({ url, params });
};
/**
* 保存部门数据权限
*/
export const saveDepartDataRule = (params) => defHttp.post({ url: Api.dataRule, params });
/**
* 获取登录用户部门信息
*/
export const getUserDeparts = (params?) => defHttp.get({ url: Api.getCurrentUserDeparts, params });
/**
* 切换选择部门
*/
export const selectDepart = (params?) => defHttp.put({ url: Api.selectDepart, params });
/**
* 编辑部门前获取部门相关信息
* @param id
*/
export const getUpdateDepartInfo = (id) => defHttp.get({ url: Api.getUpdateDepartInfo, params: {id} });
/**
* 编辑部门
* @param params
*/
export const doUpdateDepartInfo = (params) => defHttp.put({ url: Api.doUpdateDepartInfo, params });
/**
* 删除部门
* @param id
*/
export const deleteDepart = (id) => defHttp.delete({ url: Api.delete, params:{ id } }, { joinParamsToUrl: true });
/**
* 设置负责人 取消负责人
* @param params
*/
export const changeDepartChargePerson = (params) => defHttp.put({ url: Api.changeDepartChargePerson, params });

View File

@ -0,0 +1,90 @@
import { FormSchema } from '/@/components/Form';
// 部门基础表单
export function useBasicFormSchema() {
const basicFormSchema: FormSchema[] = [
{
field: 'departName',
label: '机构名称',
component: 'Input',
componentProps: {
placeholder: '请输入机构/部门名称',
},
rules: [{ required: true, message: '机构名称不能为空' }],
},
{
field: 'parentId',
label: '上级部门',
component: 'TreeSelect',
componentProps: {
treeData: [],
placeholder: '无',
dropdownStyle: { maxHeight: '200px', overflow: 'auto' },
},
},
{
field: 'orgCode',
label: '机构编码',
component: 'Input',
componentProps: {
placeholder: '请输入机构编码',
},
},
{
field: 'orgCategory',
label: '机构类型',
component: 'RadioButtonGroup',
componentProps: { options: [] },
},
{
field: 'departOrder',
label: '排序',
component: 'InputNumber',
componentProps: {},
},
{
field: 'mobile',
label: '电话',
component: 'Input',
componentProps: {
placeholder: '请输入电话',
},
},
{
field: 'fax',
label: '传真',
component: 'Input',
componentProps: {
placeholder: '请输入传真',
},
},
{
field: 'address',
label: '地址',
component: 'Input',
componentProps: {
placeholder: '请输入地址',
},
},
{
field: 'memo',
label: '备注',
component: 'InputTextArea',
componentProps: {
placeholder: '请输入备注',
},
},
];
return { basicFormSchema };
}
// 机构类型选项
export const orgCategoryOptions = {
// 一级部门
root: [{ value: '1', label: '公司' }],
// 子级部门
child: [
{ value: '2', label: '部门' },
{ value: '3', label: '岗位' },
],
};

View File

@ -0,0 +1,14 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-depart-manage';
.@{prefix-cls} {
// update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
background: @component-background;
// update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
&--box {
.ant-tabs-nav {
padding: 0 20px;
}
}
}

View File

@ -0,0 +1,63 @@
<template>
<a-row :class="['p-4', `${prefixCls}--box`]" type="flex" :gutter="10">
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<DepartLeftTree ref="leftTree" @select="onTreeSelect" @rootTreeData="onRootTreeData" />
</a-col>
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<div style="height: 100%;" :class="[`${prefixCls}`]">
<a-tabs v-show="departData != null" defaultActiveKey="base-info">
<a-tab-pane tab="基本信息" key="base-info" forceRender style="position: relative">
<div style="padding: 20px">
<DepartFormTab :data="departData" :rootTreeData="rootTreeData" @success="onSuccess" />
</div>
</a-tab-pane>
<a-tab-pane tab="部门权限" key="role-info">
<div style="padding: 0 20px 20px">
<DepartRuleTab :data="departData" />
</div>
</a-tab-pane>
</a-tabs>
<div v-show="departData == null" style="padding-top: 40px">
<a-empty description="尚未选择部门" />
</div>
</div>
</a-col>
</a-row>
</template>
<script lang="ts" setup name="system-depart">
import { provide, ref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import DepartLeftTree from './components/DepartLeftTree.vue';
import DepartFormTab from './components/DepartFormTab.vue';
import DepartRuleTab from './components/DepartRuleTab.vue';
const { prefixCls } = useDesign('depart-manage');
provide('prefixCls', prefixCls);
// 给子组件定义一个ref变量
const leftTree = ref();
// 当前选中的部门信息
const departData = ref({});
const rootTreeData = ref<any[]>([]);
// 左侧树选择后触发
function onTreeSelect(data) {
console.log('onTreeSelect: ', data);
departData.value = data;
}
// 左侧树rootTreeData触发
function onRootTreeData(data) {
rootTreeData.value = data;
}
function onSuccess() {
leftTree.value.loadRootTreeData();
}
</script>
<style lang="less">
@import './index.less';
</style>

View File

@ -0,0 +1,40 @@
<template>
<Description @register="registerDesc" />
</template>
<script lang="ts" setup>
import { ref, inject, onMounted, watch } from 'vue';
import { queryIdTree } from '../depart.user.api';
import { useBaseInfoForm } from '../depart.user.data';
import { Description, useDescription } from '/@/components/Description/index';
const prefixCls = inject('prefixCls');
const props = defineProps({
data: { require: true, type: Object },
});
const treeData = ref([]);
const { descItems } = useBaseInfoForm(treeData);
const [registerDesc, { setDescProps }] = useDescription({
data: props.data,
schema: descItems,
column: 1,
labelStyle: {
width: '180px',
},
});
function setData(data) {
setDescProps({ data });
}
onMounted(() => {
watch(
() => props.data,
() => setData(props.data),
{ immediate: true }
);
});
// 动态查询 parentId 组件的 treeData
queryIdTree().then((data) => (treeData.value = data));
</script>

View File

@ -0,0 +1,161 @@
<template>
<BasicDrawer
title="部门角色权限配置"
:width="650"
:loading="loading"
showFooter
okText="保存并关闭"
@ok="onSubmit(true)"
@close="onClose"
@register="registerDrawer"
>
<div>
<a-spin :spinning="loading">
<template v-if="treeData.length > 0">
<BasicTree
title="所拥有的部门权限"
toolbar
checkable
:treeData="treeData"
:checkedKeys="checkedKeys"
:selectedKeys="selectedKeys"
:expandedKeys="expandedKeys"
:checkStrictly="checkStrictly"
:clickRowToExpand="false"
@check="onCheck"
@expand="onExpand"
@select="onSelect"
>
<template #title="{ slotTitle, ruleFlag }">
<span>{{ slotTitle }}</span>
<Icon v-if="ruleFlag" icon="ant-design:align-left-outlined" style="margin-left: 5px; color: red" />
</template>
</BasicTree>
</template>
<a-empty v-else description="无可配置部门权限" />
</a-spin>
</div>
<template #centerFooter>
<a-button type="primary" :loading="loading" ghost @click="onSubmit(false)">仅保存</a-button>
</template>
</BasicDrawer>
<DepartRoleDataRuleDrawer @register="registerDataRuleDrawer" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { BasicTree } from '/@/components/Tree/index';
import { BasicDrawer, useDrawer, useDrawerInner } from '/@/components/Drawer';
import { useMessage } from '/@/hooks/web/useMessage';
import DepartRoleDataRuleDrawer from './DepartRoleDataRuleDrawer.vue';
import { queryTreeListForDeptRole, queryDeptRolePermission, saveDeptRolePermission } from '../depart.user.api';
import { translateTitle } from "@/utils/common/compUtils";
defineEmits(['register']);
const { createMessage } = useMessage();
const loading = ref(false);
const departId = ref('');
const roleId = ref('');
const treeData = ref<Array<any>>([]);
const checkedKeys = ref<Array<any>>([]);
const lastCheckedKeys = ref<Array<any>>([]);
const expandedKeys = ref<Array<any>>([]);
const selectedKeys = ref<Array<any>>([]);
const allTreeKeys = ref<Array<any>>([]);
const checkStrictly = ref(true);
// 注册抽屉组件
const [registerDrawer, { closeDrawer }] = useDrawerInner((data) => {
roleId.value = data.record.id;
departId.value = data.record.departId;
loadData();
});
// 注册数据规则授权弹窗抽屉
const [registerDataRuleDrawer, dataRuleDrawer] = useDrawer();
async function loadData() {
try {
loading.value = true;
// 用户角色授权功能,查询菜单权限树
const { ids, treeList } = await queryTreeListForDeptRole({ departId: departId.value });
if (ids.length > 0) {
allTreeKeys.value = ids;
expandedKeys.value = ids;
//update-begin---author:wangshuai---date:2024-04-08---for:【issues/1169】我的部门功能中的【部门权限】中未翻译 t('') 多语言---
treeData.value = translateTitle(treeList);
//update-end---author:wangshuai---date:2024-04-08---for:【issues/1169】我的部门功能中的【部门权限】中未翻译 t('') 多语言---
// 查询角色授权
checkedKeys.value = await queryDeptRolePermission({ roleId: roleId.value });
lastCheckedKeys.value = [checkedKeys.value];
} else {
reset();
}
} finally {
loading.value = false;
}
}
// 重置页面
function reset() {
treeData.value = [];
expandedKeys.value = [];
checkedKeys.value = [];
lastCheckedKeys.value = [];
loading.value = false;
}
// tree勾选复选框事件
function onCheck(event) {
if (checkStrictly.value) {
checkedKeys.value = event.checked;
} else {
checkedKeys.value = event;
}
}
// tree展开事件
function onExpand($expandedKeys) {
expandedKeys.value = $expandedKeys;
}
// tree选中事件
function onSelect($selectedKeys, { selectedNodes }) {
if (selectedNodes[0]?.ruleFlag) {
let functionId = $selectedKeys[0];
dataRuleDrawer.openDrawer(true, { roleId, departId, functionId });
}
selectedKeys.value = [];
}
function doClose() {
reset();
closeDrawer();
}
function onClose() {
reset();
}
async function onSubmit(exit) {
try {
loading.value = true;
let params = {
roleId: roleId.value,
permissionIds: checkedKeys.value.join(','),
lastpermissionIds: lastCheckedKeys.value.join(','),
};
await saveDeptRolePermission(params);
if (exit) {
doClose();
}
} finally {
loading.value = false;
if (!exit) {
loadData();
}
}
}
</script>

View File

@ -0,0 +1,82 @@
<template>
<BasicDrawer title="数据规则/按钮权限配置" :width="365" @close="onClose" @register="registerDrawer">
<a-spin :spinning="loading">
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="数据规则" key="1">
<a-checkbox-group v-model:value="dataRuleChecked" v-if="dataRuleList.length > 0">
<a-row>
<a-col :span="24" v-for="(item, index) in dataRuleList" :key="'dr' + index">
<a-checkbox :value="item.id">{{ item.ruleName }}</a-checkbox>
</a-col>
<a-col :span="24">
<div style="width: 100%; margin-top: 15px">
<a-button type="primary" :loading="loading" :size="'small'" preIcon="ant-design:save-filled" @click="saveDataRuleForRole">
<span>点击保存</span>
</a-button>
</div>
</a-col>
</a-row>
</a-checkbox-group>
<a-empty v-else description="无配置信息" />
</a-tab-pane>
<!--<a-tab-pane tab="按钮权限" key="2">敬请期待!!!</a-tab-pane>-->
</a-tabs>
</a-spin>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { queryDepartRoleDataRule, saveDepartRoleDataRule } from '../depart.user.api';
defineEmits(['register']);
const loading = ref<boolean>(false);
const departId = ref('');
const functionId = ref('');
const roleId = ref('');
const dataRuleList = ref<Array<any>>([]);
const dataRuleChecked = ref<Array<any>>([]);
// 注册抽屉组件
const [registerDrawer, { closeDrawer }] = useDrawerInner((data) => {
roleId.value = unref(data.roleId);
departId.value = unref(data.departId);
functionId.value = unref(data.functionId);
loadData();
});
async function loadData() {
try {
loading.value = true;
const { datarule, drChecked } = await queryDepartRoleDataRule(functionId, departId, roleId);
dataRuleList.value = datarule;
if (drChecked) {
dataRuleChecked.value = drChecked.split(',');
}
} finally {
loading.value = false;
}
}
function saveDataRuleForRole() {
let params = {
permissionId: functionId.value,
roleId: roleId.value,
dataRuleIds: dataRuleChecked.value.join(','),
};
saveDepartRoleDataRule(params);
}
function onClose() {
doReset();
}
function doReset() {
functionId.value = '';
roleId.value = '';
dataRuleList.value = [];
dataRuleChecked.value = [];
}
</script>

View File

@ -0,0 +1,203 @@
<template>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="addDepartRole">添加部门角色</a-button>
<template v-if="selectedRowKeys.length > 0">
<a-divider type="vertical" />
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="onDeleteDepartRoleBatch">
<icon icon="ant-design:delete-outlined" />
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作 </span>
<icon icon="akar-icons:chevron-down" />
</a-button>
</a-dropdown>
</template>
</template>
<!-- 插槽行内操作按钮 -->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<!-- 添加部门弹窗 -->
<DepartRoleModal :departId="departId" @register="registerFormModal" @success="onFormModalSuccess" />
<DepartRoleAuthDrawer @register="registerAuthDrawer" />
</template>
<script lang="ts" setup>
import { inject, ref, unref, watch, computed, onMounted } from 'vue';
import { ActionItem, BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import DepartRoleModal from './DepartRoleModal.vue';
import DepartRoleAuthDrawer from './DepartRoleAuthDrawer.vue';
import { deleteBatchDepartRole, departRoleList } from '../depart.user.api';
import { departRoleColumns, departRoleSearchFormSchema } from '../depart.user.data';
import { ColEx } from '/@/components/Form/src/types';
const prefixCls = inject('prefixCls');
const props = defineProps({
data: { require: true, type: Object },
});
defineEmits(['register']);
// 当前选中的部门ID可能会为空代表未选择部门
const departId = computed(() => props.data?.id);
// 自适应列配置
const adaptiveColProps: Partial<ColEx> = {
xs: 24, // <576px
sm: 24, // ≥576px
md: 24, // ≥768px
lg: 12, // ≥992px
xl: 12, // ≥1200px
xxl: 8, // ≥1600px
};
// 列表页面公共参数、方法
const { tableContext, createMessage } = useListPage({
tableProps: {
api: departRoleList,
columns: departRoleColumns,
canResize: false,
formConfig: {
labelWidth: 100,
schemas: departRoleSearchFormSchema,
baseColProps: adaptiveColProps,
labelAlign: 'left',
labelCol: {
xs: 24,
sm: 24,
md: 24,
lg: 9,
xl: 7,
xxl: 6,
},
wrapperCol: {},
// 操作按钮配置
actionColOptions: {
...adaptiveColProps,
style: { textAlign: 'left' },
},
},
// 【issues/1064】列设置的 cacheKey
tableSetting: { cacheKey: 'depart_user_departInfo' },
// 请求之前对参数做处理
beforeFetch(params) {
params.deptId = departId.value;
},
// update-begin--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
immediate: !!departId.value,
// update-end--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
},
});
// 注册 ListTable
const [registerTable, { reload, setProps, setLoading, updateTableDataRecord }, { rowSelection, selectedRowKeys }] = tableContext;
// 注册Form弹窗
const [registerFormModal, formModal] = useModal();
// 注册授权弹窗抽屉
const [registerAuthDrawer, authDrawer] = useDrawer();
// 监听 data 更改,重新加载数据
watch(
() => props.data,
() => reload()
);
onMounted(() => {
// update-begin--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
// reload();
// update-end--author:liaozhiyang---date:20240517---for【TV360X-53】未选择部门的情况下部门角色全查出来了
});
// 清空选择的行
function clearSelection() {
selectedRowKeys.value = [];
}
// 添加部门角色
function addDepartRole() {
formModal.openModal(true, {
isUpdate: false,
record: {},
});
}
// 编辑部门角色
function editDepartRole(record) {
formModal.openModal(true, {
isUpdate: true,
record: record,
});
}
// 授权部门角色
function permissionDepartRole(record) {
authDrawer.openDrawer(true, { record });
}
// 批量删除部门角色
async function deleteDepartRole(idList, confirm) {
if (!departId.value) {
createMessage.warning('请先选择一个部门');
} else {
setLoading(true);
let ids = unref(idList).join(',');
try {
await deleteBatchDepartRole({ ids }, confirm);
return reload();
} finally {
setLoading(false);
}
}
return Promise.reject();
}
// 批量删除部门角色事件
async function onDeleteDepartRoleBatch() {
try {
await deleteDepartRole(selectedRowKeys, true);
// 批量删除成功后清空选择
clearSelection();
} catch (e) {}
}
// 表单弹窗成功后的回调
function onFormModalSuccess({ isUpdate, values }) {
isUpdate ? updateTableDataRecord(values.id, values) : reload();
}
/**
* 操作栏
*/
function getTableAction(record): ActionItem[] {
return [{ label: '编辑', onClick: editDepartRole.bind(null, record) }];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record): ActionItem[] {
return [
{ label: '授权', onClick: permissionDepartRole.bind(null, record) },
{
label: '删除',
color: 'error',
popConfirm: {
title: '确认要删除吗',
confirm: deleteDepartRole.bind(null, [record.id], false),
},
},
];
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<BasicModal :title="title" :width="800" v-bind="$attrs" @ok="handleOk" @register="registerModal">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { computed, inject, ref, unref } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
// noinspection ES6UnusedImports
import { BasicModal, useModalInner } from '/@/components/Modal';
import { saveOrUpdateDepartRole } from '../depart.user.api';
import { departRoleModalFormSchema } from '../depart.user.data';
const emit = defineEmits(['success', 'register']);
const props = defineProps({
// 当前部门ID
departId: { require: true, type: String },
});
const prefixCls = inject('prefixCls');
// 当前是否是更新模式
const isUpdate = ref<boolean>(true);
// 当前的弹窗数据
const model = ref<object>({});
const title = computed(() => (isUpdate.value ? '编辑' : '新增'));
//注册表单
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: departRoleModalFormSchema,
showActionButtonGroup: false,
});
// 注册弹窗
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
isUpdate.value = unref(data?.isUpdate);
// 无论新增还是编辑,都可以设置表单值
let record = unref(data?.record);
if (typeof record === 'object') {
model.value = record;
await setFieldsValue({ ...record });
}
});
//提交事件
async function handleOk() {
try {
setModalProps({ confirmLoading: true });
let values = await validate();
values.departId = unref(props.departId);
//提交表单
await saveOrUpdateDepartRole(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success', { isUpdate: unref(isUpdate), values });
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,91 @@
<template>
<BasicDrawer title="部门角色分配" :width="365" @close="onClose" @register="registerDrawer">
<a-spin :spinning="loading">
<template v-if="desformList.length > 0">
<a-checkbox-group v-model:value="designNameValue">
<a-row>
<a-col :span="24" v-for="item of desformList">
<a-checkbox :value="item.id">{{ item.roleName }}</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
<div style="width: 100%; margin-top: 15px">
<a-button type="primary" :loading="loading" :size="'small'" preIcon="ant-design:save-filled" @click="onSubmit">
<span>点击保存</span>
</a-button>
</div>
</template>
<a-empty v-else description="无配置信息" />
</a-spin>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { queryDepartRoleByUserId, queryDepartRoleUserList, saveDepartRoleUser } from '../depart.user.api';
defineEmits(['register']);
const loading = ref<boolean>(false);
const userId = ref('');
const departId = ref('');
const oldRoleId = ref('');
const desformList = ref<Array<any>>([]);
const designNameValue = ref<Array<any>>([]);
// 注册抽屉组件
const [registerDrawer, { closeDrawer }] = useDrawerInner((data) => {
userId.value = unref(data.userId);
departId.value = unref(data.departId);
loadData();
});
async function loadData() {
try {
loading.value = true;
const params = {
departId: departId.value,
userId: userId.value,
};
// 查询 DepartRole
const [$desformList, $departRoleList] = await Promise.all([queryDepartRoleUserList(params), queryDepartRoleByUserId(params)]);
desformList.value = $desformList;
designNameValue.value = $departRoleList.map((item) => item.droleId);
oldRoleId.value = designNameValue.value.join(',');
} finally {
loading.value = false;
}
}
async function onSubmit() {
try {
loading.value = true;
await saveDepartRoleUser({
userId: userId.value,
newRoleId: designNameValue.value.join(','),
oldRoleId: oldRoleId.value,
});
doClose();
} finally {
loading.value = false;
}
}
function onClose() {
doReset();
}
function doClose() {
doReset();
closeDrawer();
}
function doReset() {
userId.value = '';
departId.value = '';
oldRoleId.value = '';
desformList.value = [];
designNameValue.value = [];
}
</script>

View File

@ -0,0 +1,149 @@
<template>
<div class="bg-white m-4 mr-0 overflow-hidden">
<a-spin :spinning="loading">
<template v-if="userIdentity === '2'">
<!--组织机构树-->
<BasicTree
v-if="!treeReloading"
title="部门列表"
toolbar
search
showLine
:checkStrictly="true"
:clickRowToExpand="false"
:treeData="treeData"
:selectedKeys="selectedKeys"
:expandedKeys="expandedKeys"
:autoExpandParent="autoExpandParent"
@select="onSelect"
@expand="onExpand"
@search="onSearch"
/>
</template>
<a-empty v-else description="普通员工无此权限" />
</a-spin>
</div>
</template>
<script lang="ts" setup>
import { inject, nextTick, ref } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { BasicTree } from '/@/components/Tree';
import { queryMyDepartTreeList, searchByKeywords } from '../depart.user.api';
const prefixCls = inject('prefixCls');
const emit = defineEmits(['select']);
const { createMessage } = useMessage();
let loading = ref<boolean>(false);
// 部门树列表数据
let treeData = ref<any[]>([]);
// 当前展开的项
let expandedKeys = ref<any[]>([]);
// 当前选中的项
let selectedKeys = ref<any[]>([]);
// 是否自动展开父级
let autoExpandParent = ref<boolean>(true);
// 用户身份
let userIdentity = ref<string>('2');
// 树组件重新加载
let treeReloading = ref<boolean>(false);
// 加载部门信息
function loadDepartTreeData() {
loading.value = true;
treeData.value = [];
queryMyDepartTreeList()
.then((res) => {
if (res.success) {
if (Array.isArray(res.result)) {
treeData.value = res.result;
userIdentity.value = res.message;
autoExpandParentNode();
}
} else {
createMessage.warning(res.message);
}
})
.finally(() => (loading.value = false));
}
loadDepartTreeData();
// 自动展开父节点,只展开一级
function autoExpandParentNode() {
let keys: Array<any> = [];
treeData.value.forEach((item, index) => {
if (item.children && item.children.length > 0) {
keys.push(item.key);
}
if (index === 0) {
// 默认选中第一个
setSelectedKey(item.id, item);
}
});
if (keys.length > 0) {
reloadTree();
expandedKeys.value = keys;
}
}
// 重新加载树组件,防止无法默认展开数据
async function reloadTree() {
await nextTick();
treeReloading.value = true;
await nextTick();
treeReloading.value = false;
}
/**
* 设置当前选中的行
*/
function setSelectedKey(key: string, data?: object) {
selectedKeys.value = [key];
if (data) {
emit('select', data);
}
}
// 搜索事件
function onSearch(value: string) {
if (value) {
loading.value = true;
searchByKeywords({ keyWord: value, myDeptSearch: '1' })
.then((result) => {
if (Array.isArray(result)) {
treeData.value = result;
} else {
createMessage.warning('未查询到部门信息');
treeData.value = [];
}
})
.finally(() => (loading.value = false));
} else {
loadDepartTreeData();
}
}
// 树选择事件
function onSelect(selKeys, event) {
if (selKeys.length > 0 && selectedKeys.value[0] !== selKeys[0]) {
setSelectedKey(selKeys[0], event.selectedNodes[0]);
} else {
// 这样可以防止用户取消选择
setSelectedKey(selectedKeys.value[0]);
}
}
// 树展开事件
function onExpand(keys) {
expandedKeys.value = keys;
autoExpandParent.value = false;
}
</script>
<style lang="less" scoped>
/*升级antd3后查询框与树贴的太近样式优化*/
:deep(.jeecg-tree-header) {
margin-bottom: 6px;
}
</style>

View File

@ -0,0 +1,231 @@
<template>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="selectAddUser">添加已有用户</a-button>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="createUser">新建用户</a-button>
<template v-if="selectedRowKeys.length > 0">
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="onUnlinkDepartUserBatch">
<icon icon="bx:bx-unlink" />
<span>取消关联</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作 </span>
<icon icon="akar-icons:chevron-down" />
</a-button>
</a-dropdown>
</template>
</template>
<!-- 插槽行内操作按钮 -->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<UserDrawer @register="registerDrawer" @success="onUserDrawerSuccess" />
<DepartRoleUserAuthDrawer @register="registerUserAuthDrawer" />
<UserSelectModal rowKey="id" @register="registerSelUserModal" @getSelectResult="onSelectUserOk" />
</template>
<script lang="ts" setup>
import { computed, inject, ref, unref, watch } from 'vue';
import { ActionItem, BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import UserDrawer from '/@/views/system/user/UserDrawer.vue';
import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue';
import DepartRoleUserAuthDrawer from './DepartRoleUserAuthDrawer.vue';
import { departUserList, linkDepartUserBatch, unlinkDepartUserBatch } from '../depart.user.api';
import { userInfoColumns, userInfoSearchFormSchema } from '../depart.user.data';
import { ColEx } from '/@/components/Form/src/types';
const prefixCls = inject('prefixCls');
const props = defineProps({
data: { require: true, type: Object },
});
// 当前选中的部门ID可能会为空代表未选择部门
const departId = computed(() => props.data?.id);
// 自适应列配置
const adaptiveColProps: Partial<ColEx> = {
xs: 24, // <576px
sm: 24, // ≥576px
md: 24, // ≥768px
lg: 12, // ≥992px
xl: 12, // ≥1200px
xxl: 8, // ≥1600px
};
// 列表页面公共参数、方法
const { tableContext, createMessage } = useListPage({
tableProps: {
api: departUserList,
columns: userInfoColumns,
canResize: false,
formConfig: {
schemas: userInfoSearchFormSchema,
baseColProps: adaptiveColProps,
labelAlign: 'left',
labelCol: {
xs: 24,
sm: 24,
md: 24,
lg: 9,
xl: 7,
xxl: 5,
},
wrapperCol: {},
// 操作按钮配置
actionColOptions: {
...adaptiveColProps,
style: { textAlign: 'left' },
},
},
// 【issues/1064】列设置的 cacheKey
tableSetting: { cacheKey: 'depart_user_userInfo' },
// 请求之前对参数做处理
beforeFetch(params) {
params.depId = departId.value;
},
},
});
// 注册 ListTable
const [registerTable, { reload, setProps, setLoading, updateTableDataRecord }, { rowSelection, selectedRowKeys }] = tableContext;
watch(
() => props.data,
() => reload()
);
//注册drawer
const [registerDrawer, { openDrawer, setDrawerProps }] = useDrawer();
const [registerUserAuthDrawer, userAuthDrawer] = useDrawer();
// 注册用户选择 modal
const [registerSelUserModal, selUserModal] = useModal();
// 清空选择的行
function clearSelection() {
selectedRowKeys.value = [];
}
// 查看部门角色
function showDepartRole(record) {
userAuthDrawer.openDrawer(true, { userId: record.id, departId });
}
// 创建用户
function createUser() {
if (!departId.value) {
createMessage.warning('请先选择一个部门');
} else {
openDrawer(true, {
isUpdate: false,
departDisabled: true,
// 初始化负责部门
nextDepartOptions: { value: props.data?.key, label: props.data?.title },
record: {
activitiSync: 1,
userIdentity: 1,
selecteddeparts: departId.value,
},
});
}
}
// 查看用户详情
function showUserDetail(record) {
openDrawer(true, {
record,
isUpdate: true,
departDisabled: true,
showFooter: false,
});
}
// 编辑用户信息
function editUserInfo(record) {
openDrawer(true, { isUpdate: true, record, departDisabled: true });
}
// 选择添加已有用户
function selectAddUser() {
selUserModal.openModal();
}
// 批量取消关联部门和用户之间的关系
async function unlinkDepartUser(idList, confirm) {
if (!departId.value) {
createMessage.warning('请先选择一个部门');
} else {
setLoading(true);
let userIds = unref(idList).join(',');
try {
await unlinkDepartUserBatch({ depId: departId.value, userIds }, confirm);
return reload();
} finally {
setLoading(false);
}
}
return Promise.reject();
}
// 批量取消关联事件
async function onUnlinkDepartUserBatch() {
try {
await unlinkDepartUser(selectedRowKeys, true);
// 批量删除成功后清空选择
clearSelection();
} catch (e) {}
}
// 选择用户成功
async function onSelectUserOk(options, userIdList) {
if (userIdList.length > 0) {
try {
setLoading(true);
await linkDepartUserBatch(departId.value, userIdList);
reload();
} finally {
setLoading(false);
}
}
}
/**
* 用户抽屉表单成功回调
*/
function onUserDrawerSuccess({ isUpdate, values }) {
isUpdate ? updateTableDataRecord(values.id, values) : reload();
}
/**
* 操作栏
*/
function getTableAction(record): ActionItem[] {
return [{ label: '编辑', onClick: editUserInfo.bind(null, record) }];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record): ActionItem[] {
return [
{ label: '部门角色', onClick: showDepartRole.bind(null, record) },
{ label: '用户详情', onClick: showUserDetail.bind(null, record) },
{
label: '取消关联',
color: 'error',
popConfirm: {
title: '确认取消关联吗',
confirm: unlinkDepartUser.bind(null, [record.id], false),
},
},
];
}
</script>

View File

@ -0,0 +1,159 @@
import { unref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
enum Api {
treeList = '/sys/sysDepart/queryMyDeptTreeList',
queryIdTree = '/sys/sysDepart/queryIdTree',
searchBy = '/sys/sysDepart/searchBy',
}
// 部门用户API
enum DepartUserApi {
list = '/sys/user/departUserList',
link = '/sys/user/editSysDepartWithUser',
unlink = '/sys/user/deleteUserInDepartBatch',
}
// 部门角色API
enum DepartRoleApi {
list = '/sys/sysDepartRole/list',
deleteBatch = '/sys/sysDepartRole/deleteBatch',
save = '/sys/sysDepartRole/add',
edit = '/sys/sysDepartRole/edit',
queryTreeListForDeptRole = '/sys/sysDepartPermission/queryTreeListForDeptRole',
queryDeptRolePermission = '/sys/sysDepartPermission/queryDeptRolePermission',
saveDeptRolePermission = '/sys/sysDepartPermission/saveDeptRolePermission',
dataRule = '/sys/sysDepartRole/datarule',
getDeptRoleList = '/sys/sysDepartRole/getDeptRoleList',
getDeptRoleByUserId = '/sys/sysDepartRole/getDeptRoleByUserId',
saveDeptRoleUser = '/sys/sysDepartRole/deptRoleUserAdd',
}
/**
* 获取部门树列表
*/
export const queryMyDepartTreeList = (params?) => defHttp.get({ url: Api.treeList, params }, { isTransformResponse: false });
/**
* 查询数据,以树结构形式加载所有部门的名称
*/
export const queryIdTree = (params?) => defHttp.get({ url: Api.queryIdTree, params });
/**
* 根据关键字搜索部门
*/
export const searchByKeywords = (params) => defHttp.get({ url: Api.searchBy, params });
/**
* 查询部门下的用户信息
*/
export const departUserList = (params) => defHttp.get({ url: DepartUserApi.list, params });
/**
* 批量添加部门和用户的关联关系
*
* @param departId 部门ID
* @param userIdList 用户ID列表
*/
export const linkDepartUserBatch = (departId: string, userIdList: string[]) =>
defHttp.post({ url: DepartUserApi.link, params: { depId: departId, userIdList } });
/**
* 批量取消部门和用户的关联关系
*/
export const unlinkDepartUserBatch = (params, confirm = false) => {
return new Promise((resolve, reject) => {
const doDelete = () => {
resolve(defHttp.delete({ url: DepartUserApi.unlink, params }, { joinParamsToUrl: true }));
};
if (confirm) {
createConfirm({
iconType: 'warning',
title: '取消关联',
content: '确定要取消关联吗?',
onOk: () => doDelete(),
onCancel: () => reject(),
});
} else {
doDelete();
}
});
};
/**
* 查询部门角色信息
*/
export const departRoleList = (params) => defHttp.get({ url: DepartRoleApi.list, params });
/**
* 保存或者更新部门角色
*/
export const saveOrUpdateDepartRole = (params, isUpdate) => {
if (isUpdate) {
return defHttp.put({ url: DepartRoleApi.edit, params });
} else {
return defHttp.post({ url: DepartRoleApi.save, params });
}
};
/**
* 批量删除部门角色
*/
export const deleteBatchDepartRole = (params, confirm = false) => {
return new Promise((resolve, reject) => {
const doDelete = () => {
resolve(defHttp.delete({ url: DepartRoleApi.deleteBatch, params }, { joinParamsToUrl: true }));
};
if (confirm) {
createConfirm({
iconType: 'warning',
title: '删除',
content: '确定要删除吗?',
onOk: () => doDelete(),
onCancel: () => reject(),
});
} else {
doDelete();
}
});
};
/**
* 用户角色授权功能,查询菜单权限树
*/
export const queryTreeListForDeptRole = (params) => defHttp.get({ url: DepartRoleApi.queryTreeListForDeptRole, params });
/**
* 查询角色授权
*/
export const queryDeptRolePermission = (params) => defHttp.get({ url: DepartRoleApi.queryDeptRolePermission, params });
/**
* 保存角色授权
*/
export const saveDeptRolePermission = (params) => defHttp.post({ url: DepartRoleApi.saveDeptRolePermission, params });
/**
* 查询部门角色数据权限列表
*/
export const queryDepartRoleDataRule = (functionId, departId, roleId, params?) => {
let url = `${DepartRoleApi.dataRule}/${unref(functionId)}/${unref(departId)}/${unref(roleId)}`;
return defHttp.get({ url, params });
};
/**
* 保存部门角色数据权限
*/
export const saveDepartRoleDataRule = (params) => defHttp.post({ url: DepartRoleApi.dataRule, params });
/**
* 查询部门角色用户授权
*/
export const queryDepartRoleUserList = (params) => defHttp.get({ url: DepartRoleApi.getDeptRoleList, params });
/**
* 根据 userId 查询部门角色用户授权
*/
export const queryDepartRoleByUserId = (params) => defHttp.get({ url: DepartRoleApi.getDeptRoleByUserId, params });
/**
* 保存部门角色用户授权
*/
export const saveDepartRoleUser = (params) => defHttp.post({ url: DepartRoleApi.saveDeptRoleUser, params });

View File

@ -0,0 +1,195 @@
import { Ref } from 'vue';
import { duplicateCheckDelay } from '/@/views/system/user/user.api';
import { BasicColumn, FormSchema } from '/@/components/Table';
import { DescItem } from '/@/components/Description';
import { findTree } from '/@/utils/common/compUtils';
// 用户信息 columns
export const userInfoColumns: BasicColumn[] = [
{
title: '用户账号',
dataIndex: 'username',
width: 150,
},
{
title: '用户名称',
dataIndex: 'realname',
width: 180,
},
{
title: '部门',
dataIndex: 'orgCode',
width: 200,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 80,
},
{
title: '电话',
dataIndex: 'phone',
width: 120,
},
];
// 用户信息查询条件表单
export const userInfoSearchFormSchema: FormSchema[] = [
{
field: 'username',
label: '用户账号',
component: 'Input',
},
];
// 部门角色 columns
export const departRoleColumns: BasicColumn[] = [
{
title: '部门角色名称',
dataIndex: 'roleName',
width: 100,
},
{
title: '部门角色编码',
dataIndex: 'roleCode',
width: 100,
},
{
title: '部门',
dataIndex: 'departId_dictText',
width: 100,
},
{
title: '备注',
dataIndex: 'description',
width: 100,
},
];
// 部门角色查询条件表单
export const departRoleSearchFormSchema: FormSchema[] = [
{
field: 'roleName',
label: '部门角色名称',
component: 'Input',
},
];
// 部门角色弹窗form表单
export const departRoleModalFormSchema: FormSchema[] = [
{
label: 'id',
field: 'id',
component: 'Input',
show: false,
},
{
field: 'roleName',
label: '部门角色名称',
component: 'Input',
rules: [
{ required: true, message: '部门角色名称不能为空!' },
{ min: 2, max: 30, message: '长度在 2 到 30 个字符', trigger: 'blur' },
],
},
{
field: 'roleCode',
label: '部门角色编码',
component: 'Input',
dynamicDisabled: ({ values }) => {
return !!values.id;
},
dynamicRules: ({ model }) => {
return [
{ required: true, message: '部门角色编码不能为空!' },
{ min: 0, max: 64, message: '长度不能超过 64 个字符', trigger: 'blur' },
{
validator: (_, value) => {
if (/[\u4E00-\u9FA5]/g.test(value)) {
return Promise.reject('部门角色编码不可输入汉字!');
}
return new Promise((resolve, reject) => {
let params = {
tableName: 'sys_depart_role',
fieldName: 'role_code',
fieldVal: value,
dataId: model.id,
};
duplicateCheckDelay(params)
.then((res) => {
res.success ? resolve() : reject(res.message || '校验失败');
})
.catch((err) => {
reject(err.message || '验证失败');
});
});
},
},
];
},
},
{
field: 'description',
label: '描述',
component: 'Input',
rules: [{ min: 0, max: 126, message: '长度不能超过 126 个字符', trigger: 'blur' }],
},
];
// 基本信息form
export function useBaseInfoForm(treeData: Ref<any[]>) {
const descItems: DescItem[] = [
{
field: 'departName',
label: '机构名称',
},
{
field: 'parentId',
label: '上级部门',
render(val) {
if (val) {
let data = findTree(treeData.value, (item) => item.key == val);
return data?.title ?? val;
}
return val;
},
},
{
field: 'orgCode',
label: '机构编码',
},
{
field: 'orgCategory',
label: '机构类型',
render(val) {
if (val === '1') {
return '公司';
} else if (val === '2') {
return '部门';
} else if (val === '3') {
return '岗位';
}
return val;
},
},
{
field: 'departOrder',
label: '排序',
},
{
field: 'mobile',
label: '手机号',
},
{
field: 'address',
label: '地址',
},
{
field: 'memo',
label: '备注',
},
];
return { descItems };
}

View File

@ -0,0 +1,48 @@
@prefix-cls: ~'@{namespace}-depart-user';
.@{prefix-cls} {
&--tree-search {
width: 100%;
margin: 10px 0 20px;
}
&--base-info-form {
@media (min-width: 576px) {
.no-border {
border: 0;
box-shadow: none;
}
.ant-select.ant-select-disabled {
.ant-select-selector {
border: 0;
color: black;
background-color: transparent;
}
.ant-select-selector,
.ant-select-selection-item {
cursor: text !important;
user-select: initial !important;
}
.ant-select-selection-search,
.ant-select-arrow {
display: none;
}
}
}
}
}
// 夜间模式样式兼容
[data-theme='dark'] .@{prefix-cls} {
&--base-info-form {
.ant-select.ant-select-disabled {
.ant-select-selector {
color: #c9d1d9;
background-color: transparent;
}
}
}
}

View File

@ -0,0 +1,49 @@
<template>
<a-row :class="['p-4', `${prefixCls}--box`]" :gutter="10">
<a-col :xl="6" :lg="8" :md="10" :sm="24" style="flex: 1">
<a-card :bordered="false" style="height: 100%">
<DepartTree @select="onTreeSelect" />
</a-card>
</a-col>
<a-col :xl="18" :lg="16" :md="14" :sm="24" style="flex: 1">
<a-card :bordered="false" style="height: 100%">
<a-tabs defaultActiveKey="user-info">
<a-tab-pane tab="基本信息" key="base-info" forceRender>
<DepartBaseInfoTab :data="departData" />
</a-tab-pane>
<a-tab-pane tab="用户信息" key="user-info">
<DepartUserInfoTab :data="departData" />
</a-tab-pane>
<a-tab-pane tab="部门角色" key="role-info">
<DepartRoleInfoTab :data="departData" />
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
</a-row>
</template>
<script lang="ts" setup name="system-depart-user">
import { provide, ref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import DepartTree from './components/DepartTree.vue';
import DepartBaseInfoTab from './components/DepartBaseInfoTab.vue';
import DepartUserInfoTab from './components/DepartUserInfoTab.vue';
import DepartRoleInfoTab from './components/DepartRoleInfoTab.vue';
const { prefixCls } = useDesign('depart-user');
provide('prefixCls', prefixCls);
// 当前选中的部门信息
let departData = ref({});
// 左侧树选择后触发
function onTreeSelect(data) {
departData.value = data;
}
</script>
<style lang="less">
@import './index.less';
</style>

View File

@ -0,0 +1,140 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" title="字典列表" width="800px">
<BasicTable @register="registerTable" :rowClassName="getRowClassName">
<template #tableTitle>
<a-button type="primary" @click="handleCreate"> 新增</a-button>
</template>
<template v-slot:bodyCell="{column, record, index}">
<template v-if="column.dataIndex ==='action'">
<TableAction :actions="getTableAction(record)" />
</template>
</template>
</BasicTable>
</BasicDrawer>
<DictItemModal @register="registerModal" @success="reload" :dictId="dictId" />
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
import { BasicTable, useTable, TableAction } from '/src/components/Table';
import { useModal } from '/src/components/Modal';
import { useDesign } from '/@/hooks/web/useDesign';
import DictItemModal from './DictItemModal.vue';
import { dictItemColumns, dictItemSearchFormSchema } from '../dict.data';
import { itemList, deleteItem } from '../dict.api';
import { ColEx } from '/@/components/Form/src/types';
const { prefixCls } = useDesign('row-invalid');
const dictId = ref('');
//字典配置model
const [registerModal, { openModal }] = useModal();
const [registerDrawer] = useDrawerInner(async (data) => {
dictId.value = data.id;
setProps({ searchInfo: { dictId: unref(dictId) } });
reload();
});
// 自适应列配置
const adaptiveColProps: Partial<ColEx> = {
xs: 24, // <576px
sm: 24, // ≥576px
md: 24, // ≥768px
lg: 12, // ≥992px
xl: 12, // ≥1200px
xxl: 8, // ≥1600px
};
const [registerTable, { reload, setProps }] = useTable({
//需要配置rowKey否则会有警告
rowKey:'dictId',
api: itemList,
columns: dictItemColumns,
formConfig: {
baseColProps: adaptiveColProps,
labelAlign: 'right',
labelCol: {
offset: 1,
xs: 24,
sm: 24,
md: 24,
lg: 9,
xl: 7,
xxl: 4,
},
wrapperCol: {},
schemas: dictItemSearchFormSchema,
autoSubmitOnEnter: true,
actionColOptions: {
span: 8
}
},
striped: true,
useSearchForm: true,
bordered: true,
showIndexColumn: false,
canResize: false,
immediate: false,
actionColumn: {
width: 100,
title: '操作',
dataIndex: 'action',
//slots: { customRender: 'action' },
fixed: undefined,
},
});
/**
* 新增
*/
function handleCreate() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑
*/
function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除
*/
async function handleDelete(record) {
await deleteItem({ id: record.id }, reload);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
function getRowClassName(record) {
return record.status == 0 ? prefixCls : '';
}
</script>
<style scoped lang="less">
@prefix-cls: ~'@{namespace}-row-invalid';
:deep(.@{prefix-cls}) {
background: #f4f4f4;
color: #bababa;
}
</style>

View File

@ -0,0 +1,126 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit" width="800px">
<!-- update-begin---author:wangshuai---date:2023-10-23---for:QQYUN-6804后台模式字典没有颜色配置--- -->
<BasicForm @register="registerForm" >
<template #itemColor="{ model, field }">
<div class="item-tool">
<div
v-for="(item,index) in Colors"
:style="{ color: item[0] }"
:class="model.itemColor===item[0]?'item-active':''"
class="item-color"
@click="itemColorClick(item)">
<div class="item-color-border"></div>
<div class="item-back" :style="{ background: item[0] }"></div>
</div>
</div>
</template>
</BasicForm>
<!-- update-end---author:wangshuai---date:2023-10-23---for:【QQYUN-6804】后台模式字典没有颜色配置--- -->
</BasicModal>
</template>
<script lang="ts" setup>
import { defineProps, ref, computed, unref, reactive } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { BasicForm, useForm } from '/src/components/Form';
import { itemFormSchema } from '../dict.data';
import { saveOrUpdateDictItem } from '../dict.api';
import { Colors } from '/@/utils/dict/DictColors.js'
// 声明Emits
const emit = defineEmits(['success', 'register']);
const props = defineProps({ dictId: String });
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: itemFormSchema,
showActionButtonGroup: false,
mergeDynamicData: props,
labelCol: {
xs: { span: 24 },
sm: { span: 4 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 18 },
},
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const getTitle = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
values.dictId = props.dictId;
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateDictItem(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
/**
* 字典颜色点击事件
*
* @param index
* @param item
* @param model
*/
function itemColorClick(item) {
console.log(item)
setFieldsValue({ itemColor: item[0] })
}
</script>
<style lang="less" scoped>
/*begin 字典颜色配置样式*/
.item-tool{
display: flex;
flex-wrap: wrap;
.item-color{
width: 18px;
display: flex;
justify-content: center;
cursor: pointer;
align-items: center;
margin-right: 10px;
}
.item-back{
width: 18px;
height: 18px;
border-radius: 50%;
}
}
.item-color-border{
visibility: hidden;
}
.item-active .item-color-border{
visibility: visible;
position: absolute;
border: 1px solid;
width: 24px;
height: 24px;
border-radius: 50%;
}
/*end 字典颜色配置样式*/
</style>

View File

@ -0,0 +1,52 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" width="550px" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { BasicForm, useForm } from '/src/components/Form';
import { formSchema } from '../dict.data';
import { saveOrUpdateDict } from '../dict.api';
// 声明Emits
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
const rowId = ref('');
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false, minHeight: 80 });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
rowId.value = data.record.id;
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const getTitle = computed(() => (!unref(isUpdate) ? '新增字典' : '编辑字典'));
//表单提交事件
async function handleSubmit() {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateDict(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success', { isUpdate: unref(isUpdate), values: { ...values, id: rowId.value } });
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,90 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="字典回收站" :showOkBtn="false" width="1000px" destroyOnClose>
<BasicTable @register="registerTable">
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, toRaw } from 'vue';
import { BasicModal, useModalInner } from '/src/components/Modal';
import { BasicTable, useTable, TableAction } from '/src/components/Table';
import { recycleBincolumns } from '../dict.data';
import { getRecycleBinList, putRecycleBin, deleteRecycleBin } from '../dict.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
const checkedKeys = ref<Array<string | number>>([]);
const [registerModal, { setModalProps, closeModal }] = useModalInner();
//注册table数据
const [registerTable, { reload }] = useTable({
api: getRecycleBinList,
columns: recycleBincolumns,
striped: true,
useSearchForm: false,
showTableSetting: false,
clickToRowSelect: false,
bordered: true,
showIndexColumn: false,
pagination: false,
tableSetting: { fullScreen: true },
canResize: false,
actionColumn: {
width: 100,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
/**
* 还原事件
*/
async function handleRevert(record) {
await putRecycleBin(record.id, reload);
emit('success');
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteRecycleBin(record.id, reload);
}
/**
* 批量还原事件
*/
function batchHandleRevert() {
handleRevert({ id: toRaw(checkedKeys.value).join(',') });
}
/**
* 批量删除事件
*/
function batchHandleDelete() {
handleDelete({ id: toRaw(checkedKeys.value).join(',') });
}
//获取操作栏事件
function getTableAction(record) {
return [
{
label: '取回',
icon: 'ant-design:redo-outlined',
popConfirm: {
title: '是否确认取回',
confirm: handleRevert.bind(null, record),
},
},
{
label: '彻底删除',
icon: 'ant-design:scissor-outlined',
color: 'error',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>

View File

@ -0,0 +1,135 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/dict/list',
save = '/sys/dict/add',
edit = '/sys/dict/edit',
duplicateCheck = '/sys/duplicate/check',
deleteDict = '/sys/dict/delete',
deleteBatch = '/sys/dict/deleteBatch',
importExcel = '/sys/dict/importExcel',
exportXls = '/sys/dict/exportXls',
recycleBinList = '/sys/dict/deleteList',
putRecycleBin = '/sys/dict/back',
deleteRecycleBin = '/sys/dict/deletePhysic',
itemList = '/sys/dictItem/list',
deleteItem = '/sys/dictItem/delete',
itemSave = '/sys/dictItem/add',
itemEdit = '/sys/dictItem/edit',
dictItemCheck = '/sys/dictItem/dictItemCheck',
refreshCache = '/sys/dict/refleshCache',
queryAllDictItems = '/sys/dict/queryAllDictItems',
}
/**
* 导出api
* @param params
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
* @param params
*/
export const getImportUrl = Api.importExcel;
/**
* 字典列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除字典
*/
export const deleteDict = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteDict, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除字典
* @param params
*/
export const batchDeleteDict = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 保存或者更新字典
* @param params
*/
export const saveOrUpdateDict = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 唯一校验
* @param params
*/
export const duplicateCheck = (params) => defHttp.get({ url: Api.duplicateCheck, params }, { isTransformResponse: false });
/**
* 字典回收站列表
* @param params
*/
export const getRecycleBinList = (params) => defHttp.get({ url: Api.recycleBinList, params });
/**
* 回收站还原
* @param params
*/
export const putRecycleBin = (id, handleSuccess) => {
return defHttp.put({ url: Api.putRecycleBin + `/${id}` }).then(() => {
handleSuccess();
});
};
/**
* 回收站删除
* @param params
*/
export const deleteRecycleBin = (id, handleSuccess) => {
return defHttp.delete({ url: Api.deleteRecycleBin + `/${id}` }).then(() => {
handleSuccess();
});
};
/**
* 字典配置列表
* @param params
*/
export const itemList = (params) => defHttp.get({ url: Api.itemList, params });
/**
* 字典配置删除
* @param params
*/
export const deleteItem = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteItem, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 保存或者更新字典配置
* @param params
*/
export const saveOrUpdateDictItem = (params, isUpdate) => {
let url = isUpdate ? Api.itemEdit : Api.itemSave;
return defHttp.post({ url: url, params });
};
/**
* 校验字典数据值
* @param params
*/
export const dictItemCheck = (params) => defHttp.get({ url: Api.dictItemCheck, params }, { isTransformResponse: false });
/**
* 刷新字典
* @param params
*/
export const refreshCache = () => defHttp.get({ url: Api.refreshCache }, { isTransformResponse: false });
/**
* 获取所有字典项
* @param params
*/
export const queryAllDictItems = () => defHttp.get({ url: Api.queryAllDictItems }, { isTransformResponse: false });

View File

@ -0,0 +1,203 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { dictItemCheck } from './dict.api';
import { rules } from '/@/utils/helper/validator';
import { h } from "vue";
export const columns: BasicColumn[] = [
{
title: '字典名称',
dataIndex: 'dictName',
width: 240,
},
{
title: '字典编码',
dataIndex: 'dictCode',
width: 240,
},
{
title: '描述',
dataIndex: 'description',
// width: 120
},
];
export const recycleBincolumns: BasicColumn[] = [
{
title: '字典名称',
dataIndex: 'dictName',
width: 120,
},
{
title: '字典编码',
dataIndex: 'dictCode',
width: 120,
},
{
title: '描述',
dataIndex: 'description',
width: 120,
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '字典名称',
field: 'dictName',
component: 'Input',
colProps: { span: 6 },
},
{
label: '字典编码',
field: 'dictCode',
component: 'Input',
colProps: { span: 6 },
},
];
export const formSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
label: '字典名称',
field: 'dictName',
required: true,
component: 'Input',
},
{
label: '字典编码',
field: 'dictCode',
component: 'Input',
dynamicDisabled: ({ values }) => {
return !!values.id;
},
dynamicRules: ({ model, schema }) => rules.duplicateCheckRule('sys_dict', 'dict_code', model, schema, true),
},
{
label: '描述',
field: 'description',
component: 'Input',
},
];
export const dictItemColumns: BasicColumn[] = [
{
title: '名称',
dataIndex: 'itemText',
width: 80,
},
{
title: '数据值',
dataIndex: 'itemValue',
width: 80,
},
{
title: '字典颜色',
dataIndex: 'itemColor',
width: 80,
align:'center',
customRender:({ text }) => {
return h('div', {
style: {"background": text, "width":"18px","height":"18px","border-radius":"50%","margin":"0 auto"}
})
}
},
];
export const dictItemSearchFormSchema: FormSchema[] = [
{
label: '名称',
field: 'itemText',
component: 'Input',
},
{
label: '状态',
field: 'status',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'dict_item_status',
stringToNumber: true,
},
},
];
export const itemFormSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
label: '名称',
field: 'itemText',
required: true,
component: 'Input',
},
{
label: '数据值',
field: 'itemValue',
component: 'Input',
dynamicRules: ({ values, model }) => {
return [
{
required: true,
validator: (_, value) => {
if (!value) {
return Promise.reject('请输入数据值');
}
if (new RegExp("[`~!@#$^&*()=|{}'.<>《》/?!¥()—【】‘;:”“。,、?]").test(value)) {
return Promise.reject('数据值不能包含特殊字符!');
}
return new Promise<void>((resolve, reject) => {
let params = {
dictId: values.dictId,
id: model.id,
itemValue: value,
};
dictItemCheck(params)
.then((res) => {
res.success ? resolve() : reject(res.message || '校验失败');
})
.catch((err) => {
reject(err.message || '验证失败');
});
});
},
},
];
},
},
{
label: '颜色值',
field: 'itemColor',
component: 'Input',
slot:'itemColor'
},
{
label: '描述',
field: 'description',
component: 'Input',
},
{
field: 'sortOrder',
label: '排序',
component: 'InputNumber',
defaultValue: 1,
},
{
field: 'status',
label: '是否启用',
defaultValue: 1,
component: 'JDictSelectTag',
componentProps: {
type: 'radioButton',
dictCode: 'dict_item_status',
stringToNumber: true,
},
},
];

View File

@ -0,0 +1,191 @@
<template>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-button type="primary" @click="handlerRefreshCache" preIcon="ant-design:sync-outlined"> 刷新缓存</a-button>
<a-button type="primary" @click="openRecycleModal(true)" preIcon="ant-design:hdd-outlined"> 回收站</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<!--字典弹窗-->
<DictModal @register="registerModal" @success="handleSuccess" />
<!--字典配置抽屉-->
<DictItemList @register="registerDrawer" />
<!--回收站弹窗-->
<DictRecycleBinModal @register="registerModal1" @success="reload" />
</template>
<script lang="ts" name="system-dict" setup>
//ts语法
import { ref, computed, unref } from 'vue';
import { BasicTable, TableAction } from '/src/components/Table';
import { useDrawer } from '/src/components/Drawer';
import { useModal } from '/src/components/Modal';
import DictItemList from './components/DictItemList.vue';
import DictModal from './components/DictModal.vue';
import DictRecycleBinModal from './components/DictRecycleBinModal.vue';
import { useMessage } from '/src/hooks/web/useMessage';
import { removeAuthCache, setAuthCache } from '/src/utils/auth';
import { columns, searchFormSchema } from './dict.data';
import { list, deleteDict, batchDeleteDict, getExportUrl, getImportUrl, refreshCache, queryAllDictItems } from './dict.api';
import { DB_DICT_DATA_KEY } from '/src/enums/cacheEnum';
import { useUserStore } from '/@/store/modules/user';
const { createMessage } = useMessage();
//字典model
const [registerModal, { openModal }] = useModal();
//字典配置drawer
const [registerDrawer, { openDrawer }] = useDrawer();
import { useListPage } from '/@/hooks/system/useListPage';
//回收站model
const [registerModal1, { openModal: openRecycleModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
designScope: 'dict-template',
tableProps: {
title: '数据字典',
api: list,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
actionColumn: {
width: 240,
},
},
//update-begin---author:wangshuai ---date:20220616 for[issues/I5AMDD]导入/导出功能,操作后提示没有传递 export.url/import.url 参数------------
exportConfig: {
name: '数据字典列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
//update-end---author:wangshuai ---date:20220616 for[issues/I5AMDD]导入/导出功能,操作后提示没有传递 export.url/import.url 参数--------------
});
//注册table数据
const [registerTable, { reload, updateTableDataRecord }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 新增事件
*/
function handleCreate() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
async function handleEdit(record: Recordable) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 详情
*/
async function handleDetail(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteDict({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteDict({ ids: selectedRowKeys.value }, reload);
}
/**
* 成功回调
*/
function handleSuccess({ isUpdate, values }) {
if (isUpdate) {
updateTableDataRecord(values.id, values);
} else {
reload();
}
}
/**
* 刷新缓存
*/
async function handlerRefreshCache() {
const result = await refreshCache();
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
// update-begin--author:liaozhiyang---date:20230908---for【QQYUN-6417】生产环境字典慢的问题
const userStore = useUserStore();
userStore.setAllDictItems(res.result);
// update-end--author:liaozhiyang---date:20230908---for【QQYUN-6417】生产环境字典慢的问题
createMessage.success('刷新缓存完成');
} else {
createMessage.error('刷新缓存失败');
}
}
/**
* 字典配置
*/
function handleItem(record) {
openDrawer(true, {
id: record.id,
});
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '字典配置',
onClick: handleItem.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '确定删除吗?',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>
<style scoped></style>

View File

@ -0,0 +1,69 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="40%">
<BasicForm @register="registerForm" :disabled="isDisabled" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './demo.data';
import { saveOrUpdateDemo, getDemoById } from './demo.api';
// 声明Emits
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
//自定义接受参数
const props = defineProps({
//是否禁用页面
isDisabled: {
type: Boolean,
default: false,
},
});
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
//labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false, showOkBtn: !props.isDisabled});
isUpdate.value = !!data?.isUpdate;
if(data.createBy){
await setFieldsValue({createBy: data.createBy})
}
if(data.createTime){
await setFieldsValue({createTime: data.createTime})
}
if (unref(isUpdate)) {
//获取详情
data.record = await getDemoById({ id: data.record.id });
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateDemo(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success', values);
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,73 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/test/jeecgDemo/list',
save = '/test/jeecgDemo/add',
edit = '/test/jeecgDemo/edit',
get = '/test/jeecgDemo/queryById',
delete = '/test/jeecgDemo/delete',
deleteBatch = '/test/jeecgDemo/deleteBatch',
exportXls = '/test/jeecgDemo/exportXls',
importExcel = '/test/jeecgDemo/importExcel',
}
/**
* 导出api
*/
export const getExportUrl = Api.exportXls;
/**
* 导入api
*/
export const getImportUrl = Api.importExcel;
/**
* 查询示例列表
* @param params
*/
export const getDemoList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 保存或者更新示例
* @param params
*/
export const saveOrUpdateDemo = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 查询示例详情
* @param params
*/
export const getDemoById = (params) => {
return defHttp.get({ url: Api.get, params });
};
/**
* 删除示例
* @param params
*/
export const deleteDemo = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除示例
* @param params
*/
export const batchDeleteDemo = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};

View File

@ -0,0 +1,223 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils';
export const columns: BasicColumn[] = [
{
title: '姓名',
dataIndex: 'name',
width: 170,
align: 'left',
resizable: true,
sorter: {
multiple:1
}
},
{
title: '关键词',
dataIndex: 'keyWord',
width: 130,
resizable: true,
},
{
title: '打卡时间',
dataIndex: 'punchTime',
width: 140,
resizable: true,
},
{
title: '工资',
dataIndex: 'salaryMoney',
width: 140,
resizable: true,
sorter: {
multiple: 2
}
},
{
title: '奖金',
dataIndex: 'bonusMoney',
width: 140,
resizable: true,
},
{
title: '性别',
dataIndex: 'sex',
sorter: {
multiple: 3
},
customRender: ({ record }) => {
return render.renderDict(record.sex, 'sex');
// let v = record.sex ? (record.sex == '1' ? '男' : '女') : '';
// return h('span', v);
},
width: 120,
resizable: true,
},
{
title: '生日',
dataIndex: 'birthday',
width: 120,
resizable: true,
},
{
title: '邮箱',
dataIndex: 'email',
width: 120,
resizable: true,
},
{
title: '个人简介',
dataIndex: 'content',
width: 120,
resizable: true,
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'name',
label: '姓名',
component: 'Input',
componentProps: {
trim: true,
},
colProps: { span: 8 },
},
{
field: 'birthday',
label: '生日',
component: 'RangePicker',
componentProps: {
valueType: 'Date'
},
colProps: { span: 8 },
},
{
field: 'age',
label: '年龄',
component: 'Input',
slot: 'age',
colProps: { span: 8 },
},
{
field: 'sex',
label: '性别',
colProps: { span: 8 },
component: 'JDictSelectTag',
componentProps: {
dictCode: 'sex',
placeholder: '请选择性别',
},
},
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: 'id',
component: 'Input',
show: false,
},
{
field: 'createBy',
label: 'createBy',
component: 'Input',
show: false,
},
{
field: 'createTime',
label: 'createTime',
component: 'Input',
show: false,
},
{
field: 'name',
label: '名字',
component: 'Input',
required: true,
componentProps: {
placeholder: '请输入名字',
},
},
{
field: 'keyWord',
label: '关键词',
component: 'Input',
componentProps: {
placeholder: '请输入关键词',
},
},
{
field: 'punchTime',
label: '打卡时间',
component: 'DatePicker',
componentProps: {
showTime: true,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择打卡时间',
},
},
{
field: 'salaryMoney',
label: '工资',
component: 'Input',
componentProps: {
placeholder: '请输入工资',
},
},
{
field: 'sex',
label: '性别',
component: 'JDictSelectTag',
defaultValue: '1',
componentProps: {
type: 'radio',
dictCode: 'sex',
placeholder: '请选择性别',
},
},
{
field: 'age',
label: '年龄',
component: 'InputNumber',
defaultValue: 1,
componentProps: {
placeholder: '请输入年龄',
},
},
{
field: 'birthday',
label: '生日',
component: 'DatePicker',
defaultValue: '',
componentProps: {
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择生日',
},
},
{
field: 'email',
label: '邮箱',
component: 'Input',
rules: [{ required: false, type: 'email', message: '邮箱格式不正确', trigger: 'blur' }],
componentProps: {
placeholder: '请输入邮箱',
},
},
{
field: 'content',
label: '个人简介 - To introduce myself',
component: 'InputTextArea',
labelLength: 4,
componentProps: {
placeholder: '请输入个人简介',
},
},
{
field: 'updateCount',
label: '乐观锁',
show: false,
component: 'Input',
},
];

View File

@ -0,0 +1,311 @@
<template>
<div>
<!--自定义查询区域-->
<div class="jeecg-basic-table-form-container" @keyup.enter="searchQuery" v-if="customSearch">
<a-form ref="formRef" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24">
<a-col :lg="8">
<a-form-item label="用户名">
<a-input placeholder="请输入名称模糊查询" v-model:value="queryParam.name"></a-input>
</a-form-item>
</a-col>
<a-col :lg="8">
<a-form-item label="年龄">
<a-input placeholder="最小年龄" type="ge" v-model:value="queryParam.age_begin" style="width: calc(50% - 15px)"></a-input>
<span>~</span>
<a-input placeholder="最大年龄" type="le" v-model:value="queryParam.age_end" style="width: calc(50% - 15px)"></a-input>
</a-form-item>
</a-col>
<template v-if="toggleSearchStatus">
<a-col :lg="8">
<a-form-item label="性别">
<JDictSelectTag v-model:value="queryParam.sex" placeholder="请选择性别" dictCode="sex" />
</a-form-item>
</a-col>
<a-col :lg="8">
<a-form-item label="选择用户">
<JDictSelectTag v-model:value="queryParam.id" placeholder="请选择用户" dictCode="demo,name,id" />
</a-form-item>
</a-col>
</template>
<span style="float: left; overflow: hidden" class="table-page-search-submitButtons">
<a-col :lg="6">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset" style="margin-left: 8px">重置</a-button>
<a @click="toggleSearchStatus = !toggleSearchStatus" style="margin-left: 8px">
{{ toggleSearchStatus ? '收起' : '展开' }}
<Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" />
</a>
</a-col>
</span>
</a-row>
</a-form>
</div>
<BasicTable @register="registerTable" :rowSelection="rowSelection" :class="{ 'p-4': customSearch }">
<template #form-age="{ model, field }">
<a-input placeholder="最小年龄" type="ge" v-model:value="min" style="width: calc(50% - 15px)" @change="ageChange(model, field)"></a-input>
<span>~</span>
<a-input placeholder="最大年龄" type="le" v-model:value="max" style="width: calc(50% - 15px)" @change="ageChange(model, field)"></a-input>
</template>
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleAdd">新增</a-button>
<a-upload name="file" :showUploadList="false" :customRequest="(file) => handleImportXls(file, getImportUrl, reload)">
<a-button preIcon="ant-design:import-outlined" type="primary">导入</a-button>
</a-upload>
<a-button preIcon="ant-design:export-outlined" type="primary" @click="handleExportXls('单表示例', getExportUrl,exportParams)">导出</a-button>
<a-button preIcon="ant-design:filter" type="primary" @click="">高级查询</a-button>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="openTab">打开Tab页</a-button>
<a-button preIcon="ant-design:retweet-outlined" type="primary" @click="customSearch = !customSearch">{{
customSearch ? '表单配置查询' : '自定义查询'
}}</a-button>
<a-button preIcon="ant-design:import-outlined" type="primary" @click="handleImport">弹窗导入</a-button>
<super-query :config="superQueryConfig" @search="handleSuperQuery"/>
<a-dropdown v-if="checkedKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon style="fontsize: 12px" icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<DemoModal @register="registerModal" @success="reload" :isDisabled="isDisabled"/>
<JImportModal @register="registerModalJimport" :url="getImportUrl" online />
</div>
</template>
<script lang="ts" setup>
import { ref, unref, reactive, toRaw, watch,computed } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import DemoModal from './DemoModal.vue';
import JImportModal from '/@/components/Form/src/jeecg/components/JImportModal.vue';
import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useMethods } from '/@/hooks/system/useMethods';
import { getDemoList, deleteDemo, batchDeleteDemo, getExportUrl, getImportUrl } from './demo.api';
import { columns, searchFormSchema } from './demo.data';
import { useGo } from '/@/hooks/web/usePage';
import { router } from '/@/router';
import { filterObj } from '/@/utils/common/compUtils';
const go = useGo();
const checkedKeys = ref<Array<string | number>>([]);
const [registerModal, { openModal }] = useModal();
const [registerModalJimport, { openModal: openModalJimport }] = useModal();
const { handleExportXls, handleImportXls } = useMethods();
const min = ref();
const max = ref();
const isDisabled = ref(false);
const [registerTable, { reload, setProps }] = useTable({
title: '单表示例',
api: getDemoList,
columns,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
fieldMapToTime: [['birthday', ['birthday_begin', 'birthday_end'], 'YYYY-MM-DD']],
fieldMapToNumber: [['age', ['age_begin', 'age_end']]],
autoAdvancedCol: 2,
actionColOptions: {
style: { textAlign: 'left' },
},
},
//自定义默认排序
defSort: {
column: 'createTime,sex',
order: 'desc',
},
striped: true,
useSearchForm: true,
showTableSetting: true,
clickToRowSelect: false,
bordered: true,
showIndexColumn: false,
tableSetting: { fullScreen: true },
canResize: false,
rowKey: 'id',
actionColumn: {
width: 180,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
/**
* 选择列配置
*/
const rowSelection = {
type: 'checkbox',
columnWidth: 40,
selectedRowKeys: checkedKeys,
onChange: onSelectChange,
};
function handleImport() {
openModalJimport(true);
}
const exportParams = computed(()=>{
let paramsForm = {};
if (checkedKeys.value && checkedKeys.value.length > 0) {
paramsForm['selections'] = checkedKeys.value.join(',');
}
return filterObj(paramsForm)
})
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
console.log("checkedKeys------>",checkedKeys)
checkedKeys.value = selectedRowKeys;
}
/**
* 新增事件
*/
function handleAdd() {
isDisabled.value = false;
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
isDisabled.value = false;
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 详情页面
*/
function handleDetail(record) {
isDisabled.value = true;
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteDemo({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteDemo({ ids: checkedKeys.value }, reload);
}
/**
* 年龄修改事件
*/
function ageChange(model, field) {
model[field] = [unref(min), unref(max)];
}
/**
* 打开tab页面
*/
function openTab() {
go(`/comp/jeecg/basic`);
}
//-----自定义查询----begin--------
const formElRef = ref();
const labelCol = reactive({
xs: { span: 24 },
sm: { span: 7 },
});
const wrapperCol = reactive({
xs: { span: 24 },
sm: { span: 16 },
});
const toggleSearchStatus = ref(false);
const customSearch = ref(false);
const queryParam = reactive({
name: '',
age_begin: '',
age_end: '',
sex: '',
id: '',
});
watch(customSearch, () => {
setProps({ useSearchForm: !unref(customSearch) });
});
function searchQuery() {
setProps({ searchInfo: toRaw(queryParam) });
reload();
}
function searchReset() {
Object.assign(queryParam, { name: '', age_begin: '', age_end: '', sex: '', id: '' });
reload();
}
//自定义查询----end---------
const superQueryConfig = reactive({
name:{ title: "名称", view: "text", type: "string", order: 1 },
sex:{ title: "性别", view: "list", type: "string", dictCode:'sex', order: 2 },
});
function handleSuperQuery(params) {
Object.keys(params).map(k=>{
queryParam[k] = params[k]
});
searchQuery();
}
</script>
<style lang="less" scoped>
.jeecg-basic-table-form-container {
.table-page-search-submitButtons {
display: block;
margin-bottom: 24px;
white-space: nowrap;
}
}
</style>

View File

@ -0,0 +1,65 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" :width="800" destroyOnClose>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './fill.rule.data';
import { saveFillRule, updateFillRule } from './fill.rule.api';
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));
// 声明Emits
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate, getFieldsValue }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 12 },
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//表单提交事件
async function handleSubmit() {
try {
let formValue = await validate();
setModalProps({ confirmLoading: true });
if (isUpdate.value) {
let allFieldsValue = getFieldsValue();
// 编辑页面 如果表单没有父级下拉框 则提交时候 validate方法不返该值 需要手动设置
if (!formValue.parentId && allFieldsValue.parentId) {
formValue.parentId = allFieldsValue.parentId;
}
await updateFillRule(formValue);
} else {
await saveFillRule(formValue);
}
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,83 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/fillRule/list',
test = '/sys/fillRule/testFillRule',
save = '/sys/fillRule/add',
edit = '/sys/fillRule/edit',
delete = '/sys/fillRule/delete',
deleteBatch = '/sys/fillRule/deleteBatch',
exportXls = '/sys/fillRule/exportXls',
importExcel = '/sys/fillRule/importExcel',
}
/**
* 导出地址
*/
export const exportUrl = Api.exportXls;
/**
* 导入地址
*/
export const importUrl = Api.importExcel;
/**
* 列表查询
* @param params
*/
export const getFillRuleList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 删除
* @param params
* @param handleSuccess
*/
export const deleteFillRule = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除
* @param params
*/
export const batchDeleteFillRule = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 规则功能测试
* @param params
*/
export const handleTest = (params) => {
return defHttp.get({ url: Api.test, params }, { isTransformResponse: false });
};
/**
* 保存
* @param params
*/
export const saveFillRule = (params) => {
return defHttp.post({ url: Api.save, params });
};
/**
* 更新
* @param params
*/
export const updateFillRule = (params) => {
return defHttp.put({ url: Api.edit, params });
};

View File

@ -0,0 +1,112 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { duplicateCheckDelay } from '/@/views/system/user/user.api';
export const columns: BasicColumn[] = [
{
title: '规则名称',
dataIndex: 'ruleName',
width: 200,
align: 'center',
},
{
title: '规则编码',
dataIndex: 'ruleCode',
width: 200,
align: 'center',
},
{
title: '规则实现类',
dataIndex: 'ruleClass',
width: 300,
align: 'center',
},
{
title: '规则参数',
dataIndex: 'ruleParams',
width: 200,
align: 'center',
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'ruleName',
label: '规则名称',
component: 'Input',
colProps: { span: 6 },
},
{
field: 'ruleCode',
label: '规则编码',
component: 'Input',
colProps: { span: 6 },
},
];
export const formSchema: FormSchema[] = [
{
label: '',
field: 'id',
component: 'Input',
show: false,
},
{
field: 'ruleName',
label: '规则名称',
component: 'Input',
required: true,
colProps: { span: 24 },
},
{
field: 'ruleCode',
label: '规则编码',
component: 'Input',
colProps: { span: 24 },
dynamicDisabled: ({ values }) => {
return !!values.id;
},
dynamicRules: ({ model }) => {
return [
{
required: true,
validator: (_, value) => {
return new Promise((resolve, reject) => {
if (!value) {
return reject('请输入规则编码!');
}
let params = {
tableName: 'sys_fill_rule',
fieldName: 'rule_code',
fieldVal: value,
dataId: model.id,
};
duplicateCheckDelay(params)
.then((res) => {
res.success ? resolve() : reject('规则编码已存在!');
})
.catch((err) => {
reject(err.message || '校验失败');
});
});
},
},
];
},
},
{
field: 'ruleClass',
label: '规则实现类',
component: 'Input',
required: true,
colProps: { span: 24 },
},
{
field: 'ruleParams',
label: '规则参数',
colProps: { span: 24 },
component: 'JAddInput',
componentProps: {
min: 0,
},
},
];

View File

@ -0,0 +1,146 @@
<template>
<div :class="prefixCls">
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleAdd">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作</span>
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<FillRuleModal @register="registerModal" @success="reload" />
</div>
</template>
<script name="system-fillrule" lang="ts" setup>
import { BasicTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { getFillRuleList, exportUrl, importUrl, deleteFillRule, batchDeleteFillRule, handleTest } from '/@/views/system/fillRule/fill.rule.api';
import { columns, searchFormSchema } from '/@/views/system/fillRule/fill.rule.data';
import { useModal } from '/@/components/Modal';
import { ActionItem } from '/@/components/Table';
const [registerModal, { openModal }] = useModal();
import FillRuleModal from '/@/views/system/fillRule/FillRuleModal.vue';
// 列表页面公共参数、方法
const { prefixCls, tableContext, createMessage, createSuccessModal, onExportXls, onImportXls } = useListPage({
designScope: 'fill-rule',
tableProps: {
title: '填值规则管理页面',
api: getFillRuleList,
columns: columns,
showIndexColumn: true,
formConfig: {
schemas: searchFormSchema,
},
},
exportConfig: {
url: exportUrl,
name: '填值规则列表',
},
importConfig: {
url: importUrl,
success: () => reload(),
},
});
// 注册 ListTable
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
console.log('record....', record);
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
console.log(12345, record);
await deleteFillRule({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteFillRule({ ids: selectedRowKeys.value }, () => {
selectedRowKeys.value = [];
reload();
});
}
/**
* 功能测试
*/
function testRule(record) {
let params = { ruleCode: record.ruleCode };
handleTest(params).then((res) => {
if (res.success) {
createSuccessModal({
title: '填值规则功能测试',
content: '生成结果:' + res.result,
});
} else {
createMessage.warn(res.message);
}
});
}
/**
* 编辑
*/
function getTableAction(record): ActionItem[] {
return [{ label: '编辑', onClick: handleEdit.bind(null, record) }];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record): ActionItem[] {
return [
{ label: '功能测试', onClick: testRule.bind(null, record) },
{
label: '删除',
color: 'error',
popConfirm: {
title: '确认要删除吗?',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>

View File

@ -0,0 +1,165 @@
<template>
<div class="aui-content">
<div class="aui-container">
<div class="aui-form">
<div class="aui-image">
<div class="aui-image-text">
<img :src="adTextImg" alt="" />
</div>
</div>
<div class="aui-formBox aui-formEwm">
<div class="aui-formWell">
<form>
<div class="aui-flex aui-form-nav investment_title" style="padding-bottom: 19px">
<div class="aui-flex-box activeNav">{{t('sys.login.qrSignInFormTitle')}}</div>
</div>
<div class="aui-form-box">
<div class="aui-account" style="padding: 30px 0">
<div class="aui-ewm">
<QrCode :value="qrCodeUrl" class="enter-x flex justify-center xl:justify-start" :width="280" />
</div>
</div>
</div>
<div class="aui-formButton">
<a class="aui-linek-code aui-link-register" @click="goBackHandleClick">{{t('sys.login.backSignIn')}}</a>
</div>
</form>
</div>
<div class="aui-flex aui-third-text">
<div class="aui-flex-box aui-third-border">
<span>{{ t('sys.login.otherSignIn') }}</span>
</div>
</div>
<div class="aui-flex" :class="`${prefixCls}-sign-in-way`">
<div class="aui-flex-box">
<div class="aui-third-login">
<a href="" title="github" @click="onThirdLogin('github')"><GithubFilled /></a>
</div>
</div>
<div class="aui-flex-box">
<div class="aui-third-login">
<a href="" title="企业微信" @click="onThirdLogin('wechat_enterprise')"><icon-font class="item-icon" type="icon-qiyeweixin3" /></a>
</div>
</div>
<div class="aui-flex-box">
<div class="aui-third-login">
<a href="" title="钉钉" @click="onThirdLogin('dingtalk')"><DingtalkCircleFilled /></a>
</div>
</div>
<div class="aui-flex-box">
<div class="aui-third-login">
<a href="" title="微信" @click="onThirdLogin('wechat_open')"><WechatFilled /></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 第三方登录相关弹框 -->
<ThirdModal ref="thirdModalRef"></ThirdModal>
</template>
<script lang="ts" setup name="mini-code-login">
import { ref, onUnmounted } from 'vue';
import { getLoginQrcode, getQrcodeToken } from '/@/api/sys/user';
import { useUserStore } from '/@/store/modules/user';
import { QrCode } from '/@/components/Qrcode/index';
import ThirdModal from '/@/views/sys/login/ThirdModal.vue';
import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png';
import adTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from "/@/hooks/web/useDesign";
import { GithubFilled, WechatFilled, DingtalkCircleFilled, createFromIconfontCN } from '@ant-design/icons-vue';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_2316098_umqusozousr.js',
});
const { prefixCls } = useDesign('minilogin');
const { t } = useI18n();
const qrCodeUrl = ref<string>('');
let timer: IntervalHandle;
const state = ref('0');
const thirdModalRef = ref();
const userStore = useUserStore();
const emit = defineEmits(['go-back', 'success', 'register']);
//加载二维码信息
function loadQrCode() {
state.value = '0';
getLoginQrcode().then((res) => {
qrCodeUrl.value = res.qrcodeId;
if (res.qrcodeId) {
openTimer(res.qrcodeId);
}
});
}
//监控扫码状态
function watchQrcodeToken(qrcodeId) {
getQrcodeToken({ qrcodeId: qrcodeId }).then((res) => {
let token = res.token;
if (token == '-2') {
//二维码过期重新获取
loadQrCode();
clearInterval(timer);
}
//扫码成功
if (res.success) {
state.value = '2';
clearInterval(timer);
setTimeout(() => {
userStore.qrCodeLogin(token);
}, 500);
}
});
}
/** 开启定时器 */
function openTimer(qrcodeId) {
watchQrcodeToken(qrcodeId);
closeTimer();
timer = setInterval(() => {
watchQrcodeToken(qrcodeId);
}, 1500);
}
/** 关闭定时器 */
function closeTimer() {
if (timer) clearInterval(timer);
}
/**
* 第三方登录
* @param type
*/
function onThirdLogin(type) {
thirdModalRef.value.onThirdLogin(type);
}
/**
* 初始化表单
*/
function initFrom() {
loadQrCode();
}
/**
* 返回
*/
function goBackHandleClick() {
emit('go-back');
closeTimer();
}
onUnmounted(() => {
closeTimer();
});
defineExpose({
initFrom,
});
</script>
<style lang="less" scoped>
@import '/@/assets/loginmini/style/home.less';
@import '/@/assets/loginmini/style/base.less';
</style>

View File

@ -0,0 +1,294 @@
<template>
<div class="aui-content">
<div class="aui-container">
<div class="aui-form">
<div class="aui-image">
<div class="aui-image-text">
<img :src="adTextImg" alt="" />
</div>
</div>
<div class="aui-formBox">
<div class="aui-formWell">
<div class="aui-step-box">
<div class="aui-step-item" :class="activeKey === 1 ? 'activeStep' : ''">
<div class="aui-step-tags">
<em>1</em>
<p>{{t('sys.login.authentication')}}</p>
</div>
</div>
<div class="aui-step-item" :class="activeKey === 2 ? 'activeStep' : ''">
<div class="aui-step-tags">
<em>2</em>
<p>{{t('sys.login.resetLoginPassword')}}</p>
</div>
</div>
<div class="aui-step-item" :class="activeKey === 3 ? 'activeStep' : ''">
<div class="aui-step-tags">
<em>3</em>
<p>{{t('sys.login.resetSuccess')}}</p>
</div>
</div>
</div>
<div class="" style="height: 230px; position: relative">
<a-form ref="formRef" :model="formData" v-if="activeKey === 1">
<!-- 身份验证 begin -->
<div class="aui-account aui-account-line aui-forgot">
<a-form-item>
<div class="aui-input-line">
<a-input type="text" :placeholder="t('sys.login.mobile')" v-model:value="formData.mobile" />
</div>
</a-form-item>
<div class="aui-input-line">
<a-form-item>
<a-input type="text" :placeholder="t('sys.login.smsCode')" v-model:value="formData.smscode" />
</a-form-item>
<div v-if="showInterval" class="aui-code-line" @click="getLoginCode">{{t('component.countdown.normalText')}}</div>
<div v-else class="aui-code-line">{{t('component.countdown.sendText',[unref(timeRuning)])}}</div>
</div>
</div>
<!-- 身份验证 end -->
</a-form>
<a-form ref="pwdFormRef" :model="pwdFormData" v-else-if="activeKey === 2">
<!-- 重置密码 begin -->
<div class="aui-account aui-account-line aui-forgot">
<a-form-item>
<div class="aui-input-line">
<a-input type="password" :placeholder="t('sys.login.passwordPlaceholder')" v-model:value="pwdFormData.password" />
</div>
</a-form-item>
<a-form-item>
<div class="aui-input-line">
<a-input type="password" :placeholder="t('sys.login.confirmPassword')" v-model:value="pwdFormData.confirmPassword" />
</div>
</a-form-item>
</div>
<!-- 重置密码 end -->
</a-form>
<!-- 重置成功 begin -->
<div class="aui-success" v-else>
<div class="aui-success-icon">
<img :src="successImg"/>
</div>
<h3>恭喜您,重置密码成功!</h3>
</div>
<!-- 重置成功 end -->
</div>
<div class="aui-formButton" style="padding-bottom: 40px">
<div class="aui-flex" v-if="activeKey === 1 || activeKey === 2">
<a class="aui-link-login aui-flex-box" @click="nextStepClick">{{t('sys.login.nextStep')}}</a>
</div>
<div class="aui-flex" v-else>
<a class="aui-linek-code aui-flex-box" @click="toLogin">{{t('sys.login.goToLogin')}}</a>
</div>
<div class="aui-flex">
<a class="aui-linek-code aui-flex-box" @click="goBack"> {{ t('sys.login.backSignIn') }}</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 图片验证码弹窗 -->
<CaptchaModal @register="captchaRegisterModal" @ok="getLoginCode" />
</template>
<script lang="ts" name="mini-forgotpad" setup>
import { reactive, ref, toRaw, unref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { SmsEnum, useFormRules, useFormValid, useLoginState } from '/@/views/sys/login/useLogin';
import { useMessage } from '/@/hooks/web/useMessage';
import { getCaptcha, passwordChange, phoneVerify } from '/@/api/sys/user';
import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png'
import adTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png'
import successImg from '/@/assets/loginmini/icon/icon-success.png'
import CaptchaModal from '@/components/jeecg/captcha/CaptchaModal.vue';
import { useModal } from "@/components/Modal";
import { ExceptionEnum } from "@/enums/exceptionEnum";
const [captchaRegisterModal, { openModal: openCaptchaModal }] = useModal();
//下一步控制
const activeKey = ref<number>(1);
const { t } = useI18n();
const { handleBackLogin } = useLoginState();
const { notification, createMessage, createErrorModal } = useMessage();
//是否显示获取验证码
const showInterval = ref<boolean>(true);
//60s
const timeRuning = ref<number>(60);
//定时器
const timer = ref<any>(null);
const formRef = ref();
const pwdFormRef = ref();
//账号数据
const accountInfo = reactive<any>({});
//手机号表单
const formData = reactive({
mobile: '',
smscode: '',
});
//密码表单
const pwdFormData = reactive<any>({
password: '',
confirmPassword: '',
});
const emit = defineEmits(['go-back', 'success', 'register']);
/**
* 下一步
*/
async function handleNext() {
if (!formData.mobile) {
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
if (!formData.smscode) {
createMessage.warn(t('sys.login.smsPlaceholder'));
return;
}
const resultInfo = await phoneVerify(
toRaw({
phone: formData.mobile,
smscode: formData.smscode,
})
);
if (resultInfo.success) {
Object.assign(accountInfo, {
username: resultInfo.result.username,
phone: formData.mobile,
smscode: formData.smscode,
});
activeKey.value = 2;
setTimeout(()=>{
pwdFormRef.value.resetFields();
},300)
} else {
notification.error({
message: '错误提示',
description: resultInfo.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
}
}
/**
* 完成修改密码
*/
async function finishedPwd() {
if (!pwdFormData.password) {
createMessage.warn(t('sys.login.passwordPlaceholder'));
return;
}
if (!pwdFormData.confirmPassword) {
createMessage.warn(t('sys.login.confirmPassword'));
return;
}
if (pwdFormData.password !== pwdFormData.confirmPassword) {
createMessage.warn(t('sys.login.diffPwd'));
return;
}
const resultInfo = await passwordChange(
toRaw({
username: accountInfo.username,
password: pwdFormData.password,
smscode: accountInfo.smscode,
phone: accountInfo.phone,
})
);
if (resultInfo.success) {
accountInfo.password = pwdFormData.password;
//修改密码
activeKey.value = 3;
} else {
//错误提示
createErrorModal({
title: t('sys.api.errorTip'),
content: resultInfo.message || t('sys.api.networkExceptionMsg'),
});
}
}
/**
* 下一步
*/
function nextStepClick() {
if (unref(activeKey) == 1) {
handleNext();
} else if (unref(activeKey) == 2) {
finishedPwd();
}
}
/**
* 去登录
*/
function toLogin() {
emit('success', { username: accountInfo.username, password: accountInfo.password });
initForm();
}
/**
* 返回
*/
function goBack() {
emit('go-back');
initForm();
}
/**
* 获取手机验证码
*/
async function getLoginCode() {
if (!formData.mobile) {
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
//update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
const result = await getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.FORGET_PASSWORD }).catch((res) =>{
if(res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE){
openCaptchaModal(true, {});
}
});
//update-end---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
if (result) {
const TIME_COUNT = 60;
if (!unref(timer)) {
timeRuning.value = TIME_COUNT;
showInterval.value = false;
timer.value = setInterval(() => {
if (unref(timeRuning) > 0 && unref(timeRuning) <= TIME_COUNT) {
timeRuning.value = timeRuning.value - 1;
} else {
showInterval.value = true;
clearInterval(unref(timer));
timer.value = null;
}
}, 1000);
}
}
}
/**
* 初始化表单
*/
function initForm() {
activeKey.value = 1;
Object.assign(formData, { phone: '', smscode: '' });
Object.assign(pwdFormData, { password: '', confirmPassword: '' });
Object.assign(accountInfo, {});
if(unref(timer)){
clearInterval(unref(timer));
timer.value = null;
showInterval.value = true;
}
setTimeout(()=>{
formRef.value.resetFields();
},300)
}
defineExpose({
initForm,
});
</script>
<style lang="less" scoped>
@import '/@/assets/loginmini/style/home.less';
@import '/@/assets/loginmini/style/base.less';
</style>

View File

@ -0,0 +1,573 @@
<template>
<div :class="prefixCls" class="login-background-img">
<AppLocalePicker class="absolute top-4 right-4 enter-x xl:text-gray-600" :showText="false"/>
<AppDarkModeToggle class="absolute top-3 right-7 enter-x" />
<div class="aui-logo" v-if="!getIsMobile">
<div>
<h3>
<img :src="logoImg" alt="jeecg" />
</h3>
</div>
</div>
<div v-else class="aui-phone-logo">
<img :src="logoImg" alt="jeecg" />
</div>
<div v-show="type === 'login'">
<div class="aui-content">
<div class="aui-container">
<div class="aui-form">
<div class="aui-image">
<div class="aui-image-text">
<img :src="adTextImg" />
</div>
</div>
<div class="aui-formBox">
<div class="aui-formWell">
<div class="aui-flex aui-form-nav investment_title">
<div class="aui-flex-box" :class="activeIndex === 'accountLogin' ? 'activeNav on' : ''" @click="loginClick('accountLogin')"
>{{ t('sys.login.signInFormTitle') }}
</div>
<div class="aui-flex-box" :class="activeIndex === 'phoneLogin' ? 'activeNav on' : ''" @click="loginClick('phoneLogin')"
>{{ t('sys.login.mobileSignInFormTitle') }}
</div>
</div>
<div class="aui-form-box" style="height: 180px">
<a-form ref="loginRef" :model="formData" v-if="activeIndex === 'accountLogin'" @keyup.enter.native="loginHandleClick">
<div class="aui-account">
<div class="aui-inputClear">
<i class="icon icon-code"></i>
<a-form-item>
<a-input class="fix-auto-fill" :placeholder="t('sys.login.userName')" v-model:value="formData.username" />
</a-form-item>
</div>
<div class="aui-inputClear">
<i class="icon icon-password"></i>
<a-form-item>
<a-input class="fix-auto-fill" type="password" :placeholder="t('sys.login.password')" v-model:value="formData.password" />
</a-form-item>
</div>
<div class="aui-inputClear">
<i class="icon icon-code"></i>
<a-form-item>
<a-input class="fix-auto-fill" type="text" :placeholder="t('sys.login.inputCode')" v-model:value="formData.inputCode" />
</a-form-item>
<div class="aui-code">
<img v-if="randCodeData.requestCodeSuccess" :src="randCodeData.randCodeImage" @click="handleChangeCheckCode" />
<img v-else style="margin-top: 2px; max-width: initial" :src="codeImg" @click="handleChangeCheckCode" />
</div>
</div>
<div class="aui-flex">
<div class="aui-flex-box">
<div class="aui-choice">
<a-input class="fix-auto-fill" type="checkbox" v-model:value="rememberMe" />
<span style="margin-left: 5px">{{ t('sys.login.rememberMe') }}</span>
</div>
</div>
<div class="aui-forget">
<a @click="forgetHandelClick"> {{ t('sys.login.forgetPassword') }}</a>
</div>
</div>
</div>
</a-form>
<a-form v-else ref="phoneFormRef" :model="phoneFormData" @keyup.enter.native="loginHandleClick">
<div class="aui-account phone">
<div class="aui-inputClear phoneClear">
<a-input class="fix-auto-fill" :placeholder="t('sys.login.mobile')" v-model:value="phoneFormData.mobile" />
</div>
<div class="aui-inputClear">
<a-input class="fix-auto-fill" :maxlength="6" :placeholder="t('sys.login.smsCode')" v-model:value="phoneFormData.smscode" />
<div v-if="showInterval" class="aui-code" @click="getLoginCode">
<a>{{ t('component.countdown.normalText') }}</a>
</div>
<div v-else class="aui-code">
<span class="aui-get-code code-shape">{{ t('component.countdown.sendText', [unref(timeRuning)]) }}</span>
</div>
</div>
</div>
</a-form>
</div>
<div class="aui-formButton">
<div class="aui-flex">
<a-button :loading="loginLoading" class="aui-link-login" type="primary" @click="loginHandleClick">
{{ t('sys.login.loginButton') }}</a-button>
</div>
<div class="aui-flex">
<a class="aui-linek-code aui-flex-box" @click="codeHandleClick">{{ t('sys.login.qrSignInFormTitle') }}</a>
</div>
<div class="aui-flex">
<a class="aui-linek-code aui-flex-box" @click="registerHandleClick">{{ t('sys.login.registerButton') }}</a>
</div>
</div>
</div>
<a-form @keyup.enter.native="loginHandleClick">
<div class="aui-flex aui-third-text">
<div class="aui-flex-box aui-third-border">
<span>{{ t('sys.login.otherSignIn') }}</span>
</div>
</div>
<div class="aui-flex" :class="`${prefixCls}-sign-in-way`">
<div class="aui-flex-box">
<div class="aui-third-login">
<a title="github" @click="onThirdLogin('github')"><GithubFilled /></a>
</div>
</div>
<div class="aui-flex-box">
<div class="aui-third-login">
<a title="企业微信" @click="onThirdLogin('wechat_enterprise')"><icon-font class="item-icon" type="icon-qiyeweixin3" /></a>
</div>
</div>
<div class="aui-flex-box">
<div class="aui-third-login">
<a title="钉钉" @click="onThirdLogin('dingtalk')"><DingtalkCircleFilled /></a>
</div>
</div>
<div class="aui-flex-box">
<div class="aui-third-login">
<a title="微信" @click="onThirdLogin('wechat_open')"><WechatFilled /></a>
</div>
</div>
</div>
</a-form>
</div>
</div>
</div>
</div>
</div>
<div v-show="type === 'forgot'" :class="`${prefixCls}-form`">
<MiniForgotpad ref="forgotRef" @go-back="goBack" @success="handleSuccess" />
</div>
<div v-show="type === 'register'" :class="`${prefixCls}-form`">
<MiniRegister ref="registerRef" @go-back="goBack" @success="handleSuccess" />
</div>
<div v-show="type === 'codeLogin'" :class="`${prefixCls}-form`">
<MiniCodelogin ref="codeRef" @go-back="goBack" @success="handleSuccess" />
</div>
<!-- 第三方登录相关弹框 -->
<ThirdModal ref="thirdModalRef"></ThirdModal>
<!-- 图片验证码弹窗 -->
<CaptchaModal @register="captchaRegisterModal" @ok="getLoginCode" />
</div>
</template>
<script lang="ts" setup name="login-mini">
import { getCaptcha, getCodeInfo } from '/@/api/sys/user';
import { computed, onMounted, reactive, ref, toRaw, unref } from 'vue';
import codeImg from '/@/assets/images/checkcode.png';
import { Rule } from '/@/components/Form';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import { SmsEnum } from '/@/views/sys/login/useLogin';
import ThirdModal from '/@/views/sys/login/ThirdModal.vue';
import MiniForgotpad from './MiniForgotpad.vue';
import MiniRegister from './MiniRegister.vue';
import MiniCodelogin from './MiniCodelogin.vue';
import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png';
import adTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png';
import { AppLocalePicker, AppDarkModeToggle } from '/@/components/Application';
import { useLocaleStore } from '/@/store/modules/locale';
import { useDesign } from "/@/hooks/web/useDesign";
import { useAppInject } from "/@/hooks/web/useAppInject";
import { GithubFilled, WechatFilled, DingtalkCircleFilled, createFromIconfontCN } from '@ant-design/icons-vue';
import CaptchaModal from '@/components/jeecg/captcha/CaptchaModal.vue';
import { useModal } from "@/components/Modal";
import { ExceptionEnum } from "@/enums/exceptionEnum";
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_2316098_umqusozousr.js',
});
const { prefixCls } = useDesign('mini-login');
const { notification, createMessage } = useMessage();
const userStore = useUserStore();
const { t } = useI18n();
const localeStore = useLocaleStore();
const showLocale = localeStore.getShowPicker;
const randCodeData = reactive<any>({
randCodeImage: '',
requestCodeSuccess: false,
checkKey: null,
});
const rememberMe = ref<string>('0');
//手机号登录还是账号登录
const activeIndex = ref<string>('accountLogin');
const type = ref<string>('login');
//账号登录表单字段
const formData = reactive<any>({
inputCode: '',
username: 'admin',
password: '123456',
});
//手机登录表单字段
const phoneFormData = reactive<any>({
mobile: '',
smscode: '',
});
const loginRef = ref();
//第三方登录弹窗
const thirdModalRef = ref();
//扫码登录
const codeRef = ref();
//是否显示获取验证码
const showInterval = ref<boolean>(true);
//60s
const timeRuning = ref<number>(60);
//定时器
const timer = ref<any>(null);
//忘记密码
const forgotRef = ref();
//注册
const registerRef = ref();
const loginLoading = ref<boolean>(false);
const { getIsMobile } = useAppInject();
const [captchaRegisterModal, { openModal: openCaptchaModal }] = useModal();
defineProps({
sessionTimeout: {
type: Boolean,
},
});
/**
* 获取验证码
*/
function handleChangeCheckCode() {
formData.inputCode = '';
randCodeData.checkKey = 1629428467008;
getCodeInfo(randCodeData.checkKey).then((res) => {
randCodeData.randCodeImage = res;
randCodeData.requestCodeSuccess = true;
});
}
/**
* 切换登录方式
*/
function loginClick(type) {
activeIndex.value = type;
}
/**
* 账号或者手机登录
*/
async function loginHandleClick() {
if (unref(activeIndex) === 'accountLogin') {
accountLogin();
} else {
//手机号登录
phoneLogin();
}
}
async function accountLogin() {
if (!formData.username) {
createMessage.warn(t('sys.login.accountPlaceholder'));
return;
}
if (!formData.password) {
createMessage.warn(t('sys.login.passwordPlaceholder'));
return;
}
try {
loginLoading.value = true;
const { userInfo } = await userStore.login(
toRaw({
password: formData.password,
username: formData.username,
captcha: formData.inputCode,
checkKey: randCodeData.checkKey,
mode: 'none', //不要默认的错误提示
})
);
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
duration: 3,
});
}
} catch (error) {
notification.error({
message: t('sys.api.errorTip'),
description: error.message || t('sys.login.networkExceptionMsg'),
duration: 3,
});
handleChangeCheckCode();
} finally {
loginLoading.value = false;
}
}
/**
* 手机号登录
*/
async function phoneLogin() {
if (!phoneFormData.mobile) {
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
if (!phoneFormData.smscode) {
createMessage.warn(t('sys.login.smsPlaceholder'));
return;
}
try {
loginLoading.value = true;
const { userInfo }: any = await userStore.phoneLogin({
mobile: phoneFormData.mobile,
captcha: phoneFormData.smscode,
mode: 'none', //不要默认的错误提示
});
if (userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${userInfo.realname}`,
duration: 3,
});
}
} catch (error) {
notification.error({
message: t('sys.api.errorTip'),
description: error.message || t('sys.login.networkExceptionMsg'),
duration: 3,
});
} finally {
loginLoading.value = false;
}
}
/**
* 获取手机验证码
*/
async function getLoginCode() {
if (!phoneFormData.mobile) {
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
//update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
const result = await getCaptcha({ mobile: phoneFormData.mobile, smsmode: SmsEnum.FORGET_PASSWORD }).catch((res) =>{
if(res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE){
openCaptchaModal(true, {});
}
});
//update-end---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
if (result) {
const TIME_COUNT = 60;
if (!unref(timer)) {
timeRuning.value = TIME_COUNT;
showInterval.value = false;
timer.value = setInterval(() => {
if (unref(timeRuning) > 0 && unref(timeRuning) <= TIME_COUNT) {
timeRuning.value = timeRuning.value - 1;
} else {
showInterval.value = true;
clearInterval(unref(timer));
timer.value = null;
}
}, 1000);
}
}
}
/**
* 第三方登录
* @param type
*/
function onThirdLogin(type) {
thirdModalRef.value.onThirdLogin(type);
}
/**
* 忘记密码
*/
function forgetHandelClick() {
type.value = 'forgot';
setTimeout(() => {
forgotRef.value.initForm();
}, 300);
}
/**
* 返回登录页面
*/
function goBack() {
activeIndex.value = 'accountLogin';
type.value = 'login';
}
/**
* 忘记密码/注册账号回调事件
* @param value
*/
function handleSuccess(value) {
Object.assign(formData, value);
Object.assign(phoneFormData, { mobile: "", smscode: "" });
type.value = 'login';
activeIndex.value = 'accountLogin';
handleChangeCheckCode();
}
/**
* 注册
*/
function registerHandleClick() {
type.value = 'register';
setTimeout(() => {
registerRef.value.initForm();
}, 300);
}
/**
* 注册
*/
function codeHandleClick() {
type.value = 'codeLogin';
setTimeout(() => {
codeRef.value.initFrom();
}, 300);
}
onMounted(() => {
//加载验证码
handleChangeCheckCode();
});
</script>
<style lang="less" scoped>
@import '/@/assets/loginmini/style/home.less';
@import '/@/assets/loginmini/style/base.less';
:deep(.ant-input:focus) {
box-shadow: none;
}
.aui-get-code {
float: right;
position: relative;
z-index: 3;
background: #ffffff;
color: #1573e9;
border-radius: 100px;
padding: 5px 16px;
margin: 7px;
border: 1px solid #1573e9;
top: 12px;
}
.aui-get-code:hover {
color: #1573e9;
}
.code-shape {
border-color: #dadada !important;
color: #aaa !important;
}
:deep(.jeecg-dark-switch){
position:absolute;
margin-right: 10px;
}
.aui-link-login{
height: 42px;
padding: 10px 15px;
font-size: 14px;
border-radius: 8px;
margin-top: 15px;
margin-bottom: 8px;
flex: 1;
color: #fff;
}
.aui-phone-logo{
position: absolute;
margin-left: 10px;
width: 60px;
top:2px;
z-index: 4;
}
.top-3{
top: 0.45rem;
}
</style>
<style lang="less">
@prefix-cls: ~'@{namespace}-mini-login';
@dark-bg: #293146;
html[data-theme='dark'] {
.@{prefix-cls} {
background-color: @dark-bg !important;
background-image: none;
&::before {
background-image: url(/@/assets/svg/login-bg-dark.svg);
}
.aui-inputClear{
background-color: #232a3b !important;
}
.ant-input,
.ant-input-password {
background-color: #232a3b !important;
}
.ant-btn:not(.ant-btn-link):not(.ant-btn-primary) {
border: 1px solid #4a5569 !important;
}
&-form {
background: @dark-bg !important;
}
.app-iconify {
color: #fff !important;
}
.aui-inputClear input,.aui-input-line input,.aui-choice{
color: #c9d1d9 !important;
}
.aui-formBox{
background-color: @dark-bg !important;
}
.aui-third-text span{
background-color: @dark-bg !important;
}
.aui-form-nav .aui-flex-box{
color: #c9d1d9 !important;
}
.aui-formButton .aui-linek-code{
background: @dark-bg !important;
color: white !important;
}
.aui-code-line{
border-left: none !important;
}
.ant-checkbox-inner,.aui-success h3{
border-color: #c9d1d9;
}
//update-begin---author:wangshuai ---date:20230828 for【QQYUN-6363】这个样式代码有问题不在里面导致表达式有问题------------
&-sign-in-way {
.anticon {
font-size: 22px !important;
color: #888 !important;
cursor: pointer !important;
&:hover {
color: @primary-color !important;
}
}
}
//update-end---author:wangshuai ---date:20230828 for【QQYUN-6363】这个样式代码有问题不在里面导致表达式有问题------------
}
input.fix-auto-fill,
.fix-auto-fill input {
-webkit-text-fill-color: #c9d1d9 !important;
box-shadow: inherit !important;
}
.ant-divider-inner-text {
font-size: 12px !important;
color: @text-color-secondary !important;
}
.aui-third-login a{
background: transparent;
}
}
</style>

View File

@ -0,0 +1,278 @@
<template>
<div class="aui-content">
<div class="aui-container">
<div class="aui-form">
<div class="aui-image">
<div class="aui-image-text">
<img :src="jeecgAdTextImg" alt="" />
</div>
</div>
<div class="aui-formBox">
<div class="aui-formWell">
<a-form ref="formRef" :model="formData">
<div class="aui-flex aui-form-nav aui-clear-left" style="padding-bottom: 21px">
<div class="aui-flex-box activeNav on">{{t('sys.login.signUpFormTitle')}}</div>
</div>
<div class="aui-form-box">
<div class="aui-account aui-account-line">
<a-form-item>
<div class="aui-input-line">
<Icon class="aui-icon" icon="ant-design:user-outlined"/>
<a-input class="fix-auto-fill" type="text" :placeholder="t('sys.login.userName')" v-model:value="formData.username" />
</div>
</a-form-item>
<a-form-item>
<div class="aui-input-line">
<Icon class="aui-icon" icon="ant-design:mobile-outlined"/>
<a-input class="fix-auto-fill" type="text" :placeholder="t('sys.login.mobile')" v-model:value="formData.mobile" />
</div>
</a-form-item>
<a-form-item>
<div class="aui-input-line">
<Icon class="aui-icon" icon="ant-design:mail-outlined"/>
<a-input class="fix-auto-fill" type="text" :placeholder="t('sys.login.smsCode')" v-model:value="formData.smscode" />
<div v-if="showInterval" class="aui-code-line" @click="getLoginCode">{{t('component.countdown.normalText')}}</div>
<div v-else class="aui-code-line">{{t('component.countdown.sendText',[unref(timeRuning)])}}</div>
</div>
</a-form-item>
<a-form-item>
<div class="aui-input-line">
<Icon class="aui-icon" icon="ant-design:lock-outlined"/>
<a-input class="fix-auto-fill" :type="pwdIndex==='close'?'password':'text'" :placeholder="t('sys.login.password')" v-model:value="formData.password" />
<div class="aui-eye">
<img :src="eyeKImg" alt="开启" v-if="pwdIndex==='open'" @click="pwdClick('close')" />
<img :src="eyeGImg" alt="关闭" v-else-if="pwdIndex==='close'" @click="pwdClick('open')" />
</div>
</div>
</a-form-item>
<a-form-item>
<div class="aui-input-line">
<Icon class="aui-icon" icon="ant-design:lock-outlined"/>
<a-input class="fix-auto-fill" :type="confirmPwdIndex==='close'?'password':'text'" :placeholder="t('sys.login.confirmPassword')" v-model:value="formData.confirmPassword" />
<div class="aui-eye">
<img :src="eyeKImg" alt="开启" v-if="confirmPwdIndex==='open'" @click="confirmPwdClick('close')" />
<img :src="eyeGImg" alt="关闭" v-else-if="confirmPwdIndex==='close'" @click="confirmPwdClick('open')" />
</div>
</div>
</a-form-item>
<a-form-item name="policy">
<div class="aui-flex">
<div class="aui-flex-box">
<div class="aui-choice">
<a-checkbox v-model:checked="formData.policy" />
<span style="color: #1b90ff;margin-left: 4px">{{ t('sys.login.policy') }}</span>
</div>
</div>
</div>
</a-form-item>
</div>
</div>
<div class="aui-formButton">
<div class="aui-flex">
<a class="aui-link-login aui-flex-box" @click="registerHandleClick"> {{ t('sys.login.registerButton') }}</a>
</div>
<div class="aui-flex">
<a class="aui-linek-code aui-flex-box" @click="goBackHandleClick">{{ t('sys.login.backSignIn') }}</a>
</div>
</div>
</a-form>
</div>
</div>
</div>
</div>
</div>
<!-- 图片验证码弹窗 -->
<CaptchaModal @register="captchaRegisterModal" @ok="getLoginCode" />
</template>
<script lang="ts" setup name="mini-register">
import { ref, reactive, unref, toRaw } from 'vue';
import { getCaptcha, register } from '/@/api/sys/user';
import { SmsEnum } from '/@/views/sys/login/useLogin';
import { useMessage } from '/@/hooks/web/useMessage';
import logoImg from '/@/assets/loginmini/icon/jeecg_logo.png';
import jeecgAdTextImg from '/@/assets/loginmini/icon/jeecg_ad_text.png';
import eyeKImg from '/@/assets/loginmini/icon/icon-eye-k.png';
import eyeGImg from '/@/assets/loginmini/icon/icon-eye-g.png';
import { useI18n } from "/@/hooks/web/useI18n";
import CaptchaModal from '@/components/jeecg/captcha/CaptchaModal.vue';
import { useModal } from "@/components/Modal";
import { ExceptionEnum } from "@/enums/exceptionEnum";
const { t } = useI18n();
const { notification, createErrorModal, createMessage } = useMessage();
const emit = defineEmits(['go-back', 'success', 'register']);
const formRef = ref();
const formData = reactive<any>({
username: '',
mobile: '',
smscode: '',
password: '',
confirmPassword: '',
policy: false,
});
//是否显示获取验证码
const showInterval = ref<boolean>(true);
//60s
const timeRuning = ref<number>(60);
//定时器
const timer = ref<any>(null);
//密码眼睛打开关闭
const pwdIndex = ref<string>('close');
//确认密码眼睛打开关闭
const confirmPwdIndex = ref<string>('close');
const [captchaRegisterModal, { openModal: openCaptchaModal }] = useModal();
/**
* 返回
*/
function goBackHandleClick() {
emit('go-back');
initForm();
}
/**
* 获取手机验证码
*/
async function getLoginCode() {
if (!formData.mobile) {
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
//update-begin---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
const result = await getCaptcha({ mobile: formData.mobile, smsmode: SmsEnum.REGISTER }).catch((res) =>{
if(res.code === ExceptionEnum.PHONE_SMS_FAIL_CODE){
openCaptchaModal(true, {});
}
});
//update-end---author:wangshuai---date:2024-04-18---for:【QQYUN-9005】同一个IP1分钟超过5次短信则提示需要验证码---
if (result) {
const TIME_COUNT = 60;
if (!unref(timer)) {
timeRuning.value = TIME_COUNT;
showInterval.value = false;
timer.value = setInterval(() => {
if (unref(timeRuning) > 0 && unref(timeRuning) <= TIME_COUNT) {
timeRuning.value = timeRuning.value - 1;
} else {
showInterval.value = true;
clearInterval(unref(timer));
timer.value = null;
}
}, 1000);
}
}
}
function registerHandleClick() {
if (!formData.username) {
createMessage.warn(t('sys.login.accountPlaceholder'));
return;
}
if (!formData.mobile) {
createMessage.warn(t('sys.login.mobilePlaceholder'));
return;
}
if (!formData.smscode) {
createMessage.warn(t('sys.login.smsPlaceholder'));
return;
}
if (!formData.password) {
createMessage.warn(t('sys.login.passwordPlaceholder'));
return;
}
if (!formData.confirmPassword) {
createMessage.warn(t('sys.login.confirmPassword'));
return;
}
if (formData.password !== formData.confirmPassword) {
createMessage.warn(t('sys.login.diffPwd'));
return;
}
if(!formData.policy){
createMessage.warn(t('sys.login.policyPlaceholder'));
return;
}
registerAccount();
}
/**
* 注册账号
*/
async function registerAccount() {
try {
const resultInfo = await register(
toRaw({
username: formData.username,
password: formData.password,
phone: formData.mobile,
smscode: formData.smscode,
})
);
if (resultInfo && resultInfo.data.success) {
notification.success({
description: resultInfo.data.message || t('sys.api.registerMsg'),
duration: 3,
});
emit('success', { username: formData.username, password: formData.password });
initForm();
} else {
notification.warning({
message: t('sys.api.errorTip'),
description: resultInfo.data.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
}
} catch (error) {
notification.error({
message: t('sys.api.errorTip'),
description: error.message || t('sys.api.networkExceptionMsg'),
duration: 3,
});
}
}
/**
* 初始化表单
*/
function initForm() {
Object.assign(formData,{username:'',mobile: '', smscode: '', password: '', confirmPassword: '', policy: false})
if(!unref(timer)){
showInterval.value = true;
clearInterval(unref(timer));
timer.value = null;
}
formRef.value.resetFields();
}
/**
* 密码打开或关闭
* @param value
*/
function pwdClick(value) {
pwdIndex.value = value;
}
/**
* 确认密码打开或关闭
* @param value
*/
function confirmPwdClick(value) {
confirmPwdIndex.value = value;
}
defineExpose({
initForm
})
</script>
<style lang="less" scoped>
@import '/@/assets/loginmini/style/home.less';
@import '/@/assets/loginmini/style/base.less';
.aui-input-line .aui-icon{
position: absolute;
z-index: 2;
top: 10px;
left: 10px;
font-size: 20px !important;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div> </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { isOAuth2AppEnv, sysOAuth2Login } from '/@/views/sys/login/useLogin';
import { useRouter } from 'vue-router';
import { PageEnum } from '/@/enums/pageEnum';
import { router } from '/@/router';
import { useUserStore } from '/@/store/modules/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import {getTenantId} from "/@/utils/auth";
const isOAuth = ref<boolean>(isOAuth2AppEnv());
const env = ref<any>({ thirdApp: false, wxWork: false, dingtalk: false });
const { currentRoute } = useRouter();
const route = currentRoute.value;
if (!isOAuth2AppEnv()) {
router.replace({ path: PageEnum.BASE_LOGIN, query: route.query });
}
if (isOAuth.value) {
checkEnv();
}
/**
* 检测当前的环境
*/
function checkEnv() {
// 判断当时是否是企业微信环境
if (/wxwork/i.test(navigator.userAgent)) {
env.value.thirdApp = true;
env.value.wxWork = true;
}
// 判断当时是否是钉钉环境
if (/dingtalk/i.test(navigator.userAgent)) {
env.value.thirdApp = true;
env.value.dingtalk = true;
}
doOAuth2Login();
}
/**
* 进行OAuth2登录操作
*/
function doOAuth2Login() {
if (env.value.thirdApp) {
// 判断是否携带了Token是就说明登录成功
if (route.query.oauth2LoginToken) {
let token = route.query.oauth2LoginToken;
//执行登录操作
thirdLogin({ token, thirdType: route.query.thirdType,tenantId: getTenantId });
} else if (env.value.wxWork) {
sysOAuth2Login('wechat_enterprise');
} else if (env.value.dingtalk) {
sysOAuth2Login('dingtalk');
}
}
}
/**
* 第三方登录
* @param params
*/
function thirdLogin(params) {
const userStore = useUserStore();
const { notification } = useMessage();
const { t } = useI18n();
userStore.ThirdLogin(params).then((res) => {
if (res && res.userInfo) {
notification.success({
message: t('sys.login.loginSuccessTitle'),
description: `${t('sys.login.loginSuccessDesc')}: ${res.userInfo.realname}`,
duration: 3,
});
} else {
notification.error({
message: t('sys.login.errorTip'),
description: ((res.response || {}).data || {}).message || res.message || t('sys.login.networkExceptionMsg'),
duration: 4,
});
}
});
}
</script>

View File

@ -0,0 +1,122 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" title="数据权限规则" :width="adaptiveWidth">
<BasicTable @register="registerTable">
<template #tableTitle>
<a-button type="primary" @click="handleCreate"> 新增</a-button>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
</BasicDrawer>
<DataRuleModal @register="registerModal" @success="reload" :permissionId="permissionId" />
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import DataRuleModal from './DataRuleModal.vue';
import { dataRuleColumns, dataRuleSearchFormSchema } from './menu.data';
import { dataRuleList, deleteRule } from './menu.api';
import { ColEx } from '/@/components/Form/src/types';
import { useDrawerAdaptiveWidth } from '/@/hooks/jeecg/useAdaptiveWidth';
const permissionId = ref('');
const { adaptiveWidth } = useDrawerAdaptiveWidth();
//权限规则model
const [registerModal, { openModal }] = useModal();
const [registerDrawer] = useDrawerInner(async (data) => {
permissionId.value = data.id;
setProps({ searchInfo: { permissionId: unref(permissionId) } });
reload();
});
// 自适应列配置
const adaptiveColProps: Partial<ColEx> = {
xs: 24, // <576px
sm: 24, // ≥576px
md: 24, // ≥768px
lg: 12, // ≥992px
xl: 8, // ≥1200px
xxl: 8, // ≥1600px
};
const [registerTable, { reload, setProps }] = useTable({
api: dataRuleList,
columns: dataRuleColumns,
size: 'small',
formConfig: {
baseColProps: adaptiveColProps,
labelAlign: 'right',
labelCol: {
offset: 1,
xs: 24,
sm: 24,
md: 24,
lg: 8,
xl: 8,
xxl: 8,
},
wrapperCol: {},
schemas: dataRuleSearchFormSchema,
autoSubmitOnEnter: true,
},
striped: true,
useSearchForm: true,
bordered: true,
showIndexColumn: false,
showTableSetting: false,
canResize: false,
immediate: false,
actionColumn: {
width: 100,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
/**
* 新增
*/
function handleCreate() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑
*/
function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除
*/
async function handleDelete(record) {
await deleteRule({ id: record.id }, reload);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>

View File

@ -0,0 +1,54 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit" width="700px" destroyOnClose>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { dataRuleFormSchema } from './menu.data';
import { saveOrUpdateRule } from './menu.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
const props = defineProps({ permissionId: String });
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: dataRuleFormSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const getTitle = computed(() => (!unref(isUpdate) ? '新增规则' : '编辑规则'));
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
values.permissionId = props.permissionId;
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdateRule(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,141 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" showFooter :width="adaptiveWidth" :title="getTitle" @ok="handleSubmit">
<BasicForm @register="registerForm" class="menuForm" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, computed, unref, useAttrs } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema, ComponentTypes } from './menu.data';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { list, saveOrUpdateMenu } from './menu.api';
import { useDrawerAdaptiveWidth } from '/@/hooks/jeecg/useAdaptiveWidth';
import { useI18n } from "/@/hooks/web/useI18n";
// 声明Emits
const emit = defineEmits(['success', 'register']);
const { adaptiveWidth } = useDrawerAdaptiveWidth();
const attrs = useAttrs();
const isUpdate = ref(true);
const menuType = ref(0);
const isButton = (type) => type === 2;
const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate }] = useForm({
labelCol: {
md: { span: 4 },
sm: { span: 6 },
},
wrapperCol: {
md: { span: 20 },
sm: { span: 18 },
},
schemas: formSchema,
showActionButtonGroup: false,
});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await resetFields();
setDrawerProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
menuType.value = data?.record?.menuType;
//获取下拉树信息
const treeData = await list();
updateSchema([
{
field: 'parentId',
// update-begin--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
componentProps: { treeData: translateMenu(treeData, 'name') },
// update-end--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
},
{
field: 'name',
label: isButton(unref(menuType)) ? '按钮/权限' : '菜单名称',
},
{
field: 'url',
required: !isButton(unref(menuType)),
componentProps: {
onChange: (e) => onUrlChange(e.target.value),
},
},
]);
// 无论新增还是编辑,都可以设置表单值
if (typeof data.record === 'object') {
let values = { ...data.record };
setFieldsValue(values);
onUrlChange(values.url);
}
//按钮类型情况下,编辑时候清除一下地址的校验
if (menuType.value == 2) {
clearValidate();
}
//禁用表单
setProps({ disabled: !attrs.showFooter });
});
//获取弹窗标题
const getTitle = computed(() => (!unref(isUpdate) ? '新增菜单' : '编辑菜单'));
//提交事件
async function handleSubmit() {
try {
const values = await validate();
// iframe兼容
if (ComponentTypes.IFrame === values.component) {
values.component = values.frameSrc;
}
setDrawerProps({ confirmLoading: true });
//提交表单
await saveOrUpdateMenu(values, unref(isUpdate));
closeDrawer();
emit('success');
} finally {
setDrawerProps({ confirmLoading: false });
}
}
/** url 变化时动态设置组件名称placeholder */
function onUrlChange(url) {
let placeholder = '';
let httpUrl = url;
if (url != null && url != '') {
if (url.startsWith('/')) {
url = url.substring(1);
}
url = url.replaceAll('/', '-');
// 特殊标记
url = url.replaceAll(':', '@');
placeholder = `${url}`;
} else {
placeholder = '请输入组件名称';
}
updateSchema([{ field: 'componentName', componentProps: { placeholder } }]);
//update-begin---author:wangshuai ---date:20230204 for[QQYUN-4058]菜单添加智能化处理------------
if (httpUrl != null && httpUrl != '') {
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
setFieldsValue({ component: httpUrl });
}
}
//update-end---author:wangshuai ---date:20230204 for[QQYUN-4058]菜单添加智能化处理------------
}
/**
* 2024-03-06
* liaozhiyang
* 翻译菜单名称
*/
function translateMenu(data, key) {
if (data?.length) {
const { t } = useI18n();
data.forEach((item) => {
if (item[key]) {
if (item[key].includes("t('") && t) {
item[key] = new Function('t', `return ${item[key]}`)(t);
}
}
if (item.children?.length) {
translateMenu(item.children, key);
}
});
}
return data;
}
</script>

View File

@ -0,0 +1,259 @@
<template>
<div class="p-4">
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增菜单</a-button>
<a-button type="primary" preIcon="ic:round-expand" @click="expandAll">展开全部</a-button>
<a-button type="primary" preIcon="ic:round-compress" @click="collapseAll">折叠全部</a-button>
<a-dropdown v-if="checkedKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined" />
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="ant-design:down-outlined" />
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<MenuDrawer @register="registerDrawer" @success="handleSuccess" :showFooter="showFooter" />
<DataRuleList @register="registerDrawer1" />
</div>
</template>
<script lang="ts" name="system-menu" setup>
import { nextTick, ref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { useDrawer } from '/@/components/Drawer';
import MenuDrawer from './MenuDrawer.vue';
import DataRuleList from './DataRuleList.vue';
import { columns,searchFormSchema } from './menu.data';
import { list, deleteMenu, batchDeleteMenu } from './menu.api';
import { useDefIndexStore } from "@/store/modules/defIndex";
import { useI18n } from "/@/hooks/web/useI18n";
const checkedKeys = ref<Array<string | number>>([]);
const showFooter = ref(true);
const [registerDrawer, { openDrawer }] = useDrawer();
const [registerDrawer1, { openDrawer: openDataRule }] = useDrawer();
const { t } = useI18n();
// 自定义菜单名称列渲染
columns[0].customRender = function ({text, record}) {
const isDefIndex = checkDefIndex(record)
if (isDefIndex) {
text += '(默认首页)'
}
// update-begin--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
if (text.includes("t('") && t) {
return new Function('t', `return ${text}`)(t);
}
// update-end--author:liaozhiyang---date:20240306---for【QQYUN-8379】菜单管理页菜单国际化
return text
}
// 列表页面公共参数、方法
const { prefixCls, tableContext } = useListPage({
tableProps: {
title: '菜单列表',
api: list,
columns: columns,
size: 'small',
pagination: false,
isTreeTable: true,
striped: true,
useSearchForm: true,
showTableSetting: true,
bordered: true,
showIndexColumn: false,
tableSetting: { fullScreen: true },
formConfig: {
// update-begin--author:liaozhiyang---date:20230803---for【QQYUN-5873】查询区域lablel默认居左
labelWidth:60,
owProps: { gutter: 24 },
// update-end--author:liaozhiyang---date:20230803---for【QQYUN-5873】查询区域lablel默认居左
schemas: searchFormSchema,
autoAdvancedCol: 4,
baseColProps: { xs: 24, sm: 12, md: 6, lg: 6, xl: 6, xxl: 6 },
actionColOptions: { xs: 24, sm: 12, md: 6, lg: 6, xl: 6, xxl: 6 },
},
actionColumn: {
width: 120,
},
},
});
//注册table数据
const [registerTable, { reload, expandAll, collapseAll }] = tableContext;
/**
* 选择列配置
*/
const rowSelection = {
type: 'checkbox',
columnWidth: 30,
selectedRowKeys: checkedKeys,
onChange: onSelectChange,
};
/**
* 选择事件
*/
function onSelectChange(selectedRowKeys: (string | number)[]) {
checkedKeys.value = selectedRowKeys;
}
/**
* 新增
*/
function handleCreate() {
showFooter.value = true;
openDrawer(true, {
isUpdate: false,
});
}
/**
* 编辑
*/
function handleEdit(record) {
showFooter.value = true;
openDrawer(true, {
record,
isUpdate: true,
});
}
/**
* 详情
*/
function handleDetail(record) {
showFooter.value = false;
openDrawer(true, {
record,
isUpdate: true,
});
}
/**
* 添加下级
*/
function handleAddSub(record) {
openDrawer(true, {
record: { parentId: record.id, menuType: 1 },
isUpdate: false,
});
}
/**
* 数据权限弹窗
*/
function handleDataRule(record) {
openDataRule(true, { id: record.id });
}
/**
* 删除
*/
async function handleDelete(record) {
await deleteMenu({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteMenu({ ids: checkedKeys.value }, reload);
}
/**
* 成功回调
*/
function handleSuccess() {
reload();
reloadDefIndex();
}
function onFetchSuccess() {
// 演示默认展开所有表项
nextTick(expandAll);
}
// --------------- begin 默认首页配置 ------------
const defIndexStore = useDefIndexStore()
// 设置默认主页
async function handleSetDefIndex(record: Recordable) {
defIndexStore.update(record.url, record.component, record.route)
}
/**
* 检查是否为默认主页
* @param record
*/
function checkDefIndex(record: Recordable) {
return defIndexStore.check(record.url)
}
// 重新加载默认首页配置
function reloadDefIndex() {
try {
defIndexStore.query();
} catch (e) {
console.error(e)
}
}
reloadDefIndex()
// --------------- end 默认首页配置 ------------
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
// {
// label: '详情',
// onClick: handleDetail.bind(null, record),
// },
{
label: '添加下级',
onClick: handleAddSub.bind(null, record),
},
{
label: '数据规则',
onClick: handleDataRule.bind(null, record),
},
{
label: '设为默认首页',
onClick: handleSetDefIndex.bind(null, record),
ifShow: () => !record.internalOrExternal && record.component && !checkDefIndex(record),
},
{
label: '删除',
color: 'error',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>

View File

@ -0,0 +1,122 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/permission/list',
save = '/sys/permission/add',
edit = '/sys/permission/edit',
delete = '/sys/permission/delete',
deleteBatch = '/sys/permission/deleteBatch',
ruleList = '/sys/permission/queryPermissionRule',
ruleSave = '/sys/permission/addPermissionRule',
ruleEdit = '/sys/permission/editPermissionRule',
ruleDelete = '/sys/permission/deletePermissionRule',
checkPermDuplication = '/sys/permission/checkPermDuplication',
}
/**
* 列表接口
* @param params
*/
export const list = (params) => {
return defHttp.get({ url: Api.list, params });
}
/**
* 删除菜单
*/
export const deleteMenu = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除菜单
* @param params
*/
export const batchDeleteMenu = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 保存或者更新菜单
* @param params
*/
export const saveOrUpdateMenu = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 菜单数据权限列表接口
* @param params
*/
export const dataRuleList = (params) => defHttp.get({ url: Api.ruleList, params });
/**
* 保存或者更新数据规则
* @param params
*/
export const saveOrUpdateRule = (params, isUpdate) => {
let url = isUpdate ? Api.ruleEdit : Api.ruleSave;
return defHttp.post({ url: url, params });
};
/**
* 删除数据权限
*/
export const deleteRule = (params, handleSuccess) => {
return defHttp.delete({ url: Api.ruleDelete, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 根据code获取字典数值
* @param params
*/
export const ajaxGetDictItems = (params) => defHttp.get({ url: `/sys/dict/getDictItems/${params.code}` });
/**
* 唯一校验
* @param params
*/
export const getCheckPermDuplication = (params) => defHttp.get({ url: Api.checkPermDuplication, params }, { isTransformResponse: false });
/**
* 校验菜单是否存在
* @param model
* @param schema
* @param required
*/
export const checkPermDuplication=(model, schema, required?)=>{
return [
{
validator: (_, value) => {
if (!required) {
return Promise.resolve();
}
if (!value && required) {
return Promise.reject(`请输入${schema.label}`);
}
return new Promise<void>((resolve, reject) => {
getCheckPermDuplication({
id: model.id,
url:model.url,
alwaysShow:model.alwaysShow
}).then((res) => {
res.success ? resolve() : reject(res.message || '校验失败');
}).catch((err) => {
reject(err.message || '验证失败');
});
});
},
},
];
}

View File

@ -0,0 +1,420 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
import { h } from 'vue';
import { Icon } from '/@/components/Icon';
import { duplicateCheck } from '../user/user.api';
import { ajaxGetDictItems ,checkPermDuplication } from './menu.api';
import { render } from '/@/utils/common/renderUtils';
const isDir = (type) => type === 0;
const isMenu = (type) => type === 1;
const isButton = (type) => type === 2;
// 定义可选择的组件类型
export enum ComponentTypes {
Default = 'layouts/default/index',
IFrame = 'sys/iframe/FrameBlank',
}
export const columns: BasicColumn[] = [
{
title: '菜单名称',
dataIndex: 'name',
width: 200,
align: 'left',
},
{
title: '菜单类型',
dataIndex: 'menuType',
width: 150,
customRender: ({ text }) => {
return render.renderDict(text, 'menu_type');
},
},
{
title: '图标',
dataIndex: 'icon',
width: 50,
customRender: ({ record }) => {
return h(Icon, { icon: record.icon });
},
},
{
title: '组件',
dataIndex: 'component',
align: 'left',
width: 150,
},
{
title: '路径',
dataIndex: 'url',
align: 'left',
width: 150,
},
{
title: '排序',
dataIndex: 'sortNo',
width: 50,
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'name',
label: '菜单名称',
component: 'Input',
colProps: { span: 8 },
},
];
export const formSchema: FormSchema[] = [
{
label: 'id',
field: 'id',
component: 'Input',
show: false,
},
{
field: 'menuType',
label: '菜单类型',
component: 'RadioButtonGroup',
defaultValue: 0,
componentProps: ({ formActionType, formModel }) => {
return {
options: [
{ label: '一级菜单', value: 0 },
{ label: '子菜单', value: 1 },
{ label: '按钮/权限', value: 2 },
],
onChange: (e) => {
const { updateSchema, clearValidate } = formActionType;
const label = isButton(e) ? '按钮/权限' : '菜单名称';
//清除校验
clearValidate();
updateSchema([
{
field: 'name',
label: label,
},
{
field: 'url',
required: !isButton(e),
},
]);
//update-begin---author:wangshuai ---date:20220729 for[VUEN-1834]只有一级菜单,才默认值,子菜单的时候,清空------------
if (isMenu(e) && !formModel.id && (formModel.component=='layouts/default/index' || formModel.component=='layouts/RouteView')) {
formModel.component = '';
}
//update-end---author:wangshuai ---date:20220729 for[VUEN-1834]只有一级菜单,才默认值,子菜单的时候,清空------------
},
};
},
},
{
field: 'name',
label: '菜单名称',
component: 'Input',
required: true,
},
{
field: 'parentId',
label: '上级菜单',
component: 'TreeSelect',
required: true,
componentProps: {
//update-begin---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
fieldNames: {
label: 'name',
key: 'id',
value: 'id',
},
//update-end---author:wangshuai ---date:20230829 forreplaceFields已过期使用fieldNames代替------------
dropdownStyle: {
maxHeight: '50vh',
},
getPopupContainer: (node) => node?.parentNode,
},
ifShow: ({ values }) => !isDir(values.menuType),
},
{
field: 'url',
label: '访问路径',
component: 'Input',
required: true,
//update-begin-author:liusq date:2023-06-06 for: [issues/5008]子表数据权限设置不生效
ifShow: ({ values }) => !(values.component === ComponentTypes.IFrame && values.internalOrExternal),
//update-begin-author:zyf date:2022-11-02 for: 聚合路由允许路径重复
dynamicRules: ({ model, schema,values }) => {
return checkPermDuplication(model, schema, values.menuType !== 2?true:false);
},
//update-end-author:zyf date:2022-11-02 for: 聚合路由允许路径重复
//update-end-author:liusq date:2022-06-06 for: [issues/5008]子表数据权限设置不生效
},
{
field: 'component',
label: '前端组件',
component: 'Input',
componentProps: {
placeholder: '请输入前端组件',
},
defaultValue:'layouts/default/index',
required: true,
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'componentName',
label: '组件名称',
component: 'Input',
componentProps: {
placeholder: '请输入组件名称',
},
helpMessage: [
'此处名称应和vue组件的name属性保持一致。',
'组件名称不能重复,主要用于路由缓存功能。',
'如果组件名称和vue组件的name属性不一致则会导致路由缓存失效。',
'非必填,留空则会根据访问路径自动生成。',
],
defaultValue: '',
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'frameSrc',
label: 'Iframe地址',
component: 'Input',
rules: [
{ required: true, message: '请输入Iframe地址' },
{ type: 'url', message: '请输入正确的url地址' },
],
ifShow: ({ values }) => !isButton(values.menuType) && values.component === ComponentTypes.IFrame,
},
{
field: 'redirect',
label: '默认跳转地址',
component: 'Input',
ifShow: ({ values }) => isDir(values.menuType),
},
{
field: 'perms',
label: '授权标识',
component: 'Input',
ifShow: ({ values }) => isButton(values.menuType),
// dynamicRules: ({ model }) => {
// return [
// {
// required: false,
// validator: (_, value) => {
// return new Promise((resolve, reject) => {
// let params = {
// tableName: 'sys_permission',
// fieldName: 'perms',
// fieldVal: value,
// dataId: model.id,
// };
// duplicateCheck(params)
// .then((res) => {
// res.success ? resolve() : reject(res.message || '校验失败');
// })
// .catch((err) => {
// reject(err.message || '校验失败');
// });
// });
// },
// },
// ];
// },
},
{
field: 'permsType',
label: '授权策略',
component: 'RadioGroup',
defaultValue: '1',
helpMessage: ['可见/可访问(授权后可见/可访问)', '可编辑(未授权时禁用)'],
componentProps: {
options: [
{ label: '可见/可访问', value: '1' },
{ label: '可编辑', value: '2' },
],
},
ifShow: ({ values }) => isButton(values.menuType),
},
{
field: 'status',
label: '状态',
component: 'RadioGroup',
defaultValue: '1',
componentProps: {
options: [
{ label: '有效', value: '1' },
{ label: '无效', value: '0' },
],
},
ifShow: ({ values }) => isButton(values.menuType),
},
{
field: 'icon',
label: '菜单图标',
component: 'IconPicker',
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'sortNo',
label: '排序',
component: 'InputNumber',
defaultValue: 1,
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'route',
label: '是否路由菜单',
component: 'Switch',
defaultValue: true,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'hidden',
label: '隐藏路由',
component: 'Switch',
defaultValue: 0,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'hideTab',
label: '隐藏Tab',
component: 'Switch',
defaultValue: 0,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'keepAlive',
label: '是否缓存路由',
component: 'Switch',
defaultValue: false,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'alwaysShow',
label: '聚合路由',
component: 'Switch',
defaultValue: false,
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
ifShow: ({ values }) => !isButton(values.menuType),
},
{
field: 'internalOrExternal',
label: '打开方式',
component: 'Switch',
defaultValue: false,
componentProps: {
checkedChildren: '外部',
unCheckedChildren: '内部',
},
ifShow: ({ values }) => !isButton(values.menuType),
},
];
export const dataRuleColumns: BasicColumn[] = [
{
title: '规则名称',
dataIndex: 'ruleName',
width: 150,
},
{
title: '规则字段',
dataIndex: 'ruleColumn',
width: 100,
},
{
title: '规则值',
dataIndex: 'ruleValue',
width: 100,
},
];
export const dataRuleSearchFormSchema: FormSchema[] = [
{
field: 'ruleName',
label: '规则名称',
component: 'Input',
// colProps: { span: 6 },
},
{
field: 'ruleValue',
label: '规则值',
component: 'Input',
// colProps: { span: 6 },
},
];
export const dataRuleFormSchema: FormSchema[] = [
{
label: 'id',
field: 'id',
component: 'Input',
show: false,
},
{
field: 'ruleName',
label: '规则名称',
component: 'Input',
required: true,
},
{
field: 'ruleColumn',
label: '规则字段',
component: 'Input',
ifShow: ({ values }) => {
const ruleConditions = Array.isArray(values.ruleConditions) ? values.ruleConditions[0] : values.ruleConditions;
return ruleConditions !== 'USE_SQL_RULES';
},
},
{
field: 'ruleConditions',
label: '条件规则',
required: true,
component: 'ApiSelect',
componentProps: {
api: ajaxGetDictItems,
params: { code: 'rule_conditions' },
labelField: 'text',
valueField: 'value',
getPopupContainer: (node) => document.body,
},
},
{
field: 'ruleValue',
label: '规则值',
component: 'Input',
required: true,
},
{
field: 'status',
label: '状态',
component: 'RadioButtonGroup',
defaultValue: '1',
componentProps: {
options: [
{ label: '无效', value: '0' },
{ label: '有效', value: '1' },
],
},
},
];

View File

@ -0,0 +1,172 @@
<template>
<a-list item-layout="horizontal" :data-source="messageList">
<template #loadMore>
<div
v-if="messageList && messageList.length > 0 && !loadEndStatus && !loadingMoreStatus"
:style="{ textAlign: 'center', marginTop: '12px', height: '32px', lineHeight: '32px' }"
>
<a-button @click="onLoadMore">加载更多</a-button>
</div>
<div
v-if="messageList && messageList.length > 0 && loadEndStatus"
:style="{ textAlign: 'center', marginTop: '12px', height: '32px', lineHeight: '32px' }"
>
没有更多了
</div>
</template>
<template #renderItem="{ item }">
<a-list-item>
<template #actions>
<a-rate :value="item.starFlag=='1'?1:0" :count="1" @click="clickStar(item)" style="cursor: pointer" disabled />
</template>
<a-list-item-meta :description="item.createTime">
<template #title>
<div style="position: relative">
<!-- <span style="display: inline-block; position: absolute; left: -16px">
<exclamation-outlined v-if="noRead(item)" title="未读消息" style="color: red" />
</span>-->
<span>{{ getMsgCategory(item) }}</span>
<span v-if="item.busType == 'bpm'" class="bpm-cuiban-content" v-html="item.msgContent">
</span>
<a-tooltip v-else>
<template #title>
<div v-html="item.msgContent"></div>
</template>
{{ item.titile }}
</a-tooltip>
<a @click="showMessageDetail(item)" style="margin-left: 16px">查看详情</a>
</div>
</template>
<template #avatar>
<template v-if="item.busType=='email'">
<a-badge dot v-if="noRead(item)" class="msg-no-read">
<a-avatar style="background: #79919d"><mail-outlined style="font-size: 16px" title="未读消息"/></a-avatar>
</a-badge>
<a-avatar v-else style="background: #79919d"><mail-outlined style="font-size: 16px"/></a-avatar>
</template>
<template v-else-if="item.busType=='bpm_task'">
<a-badge dot v-if="noRead(item)" class="msg-no-read">
<a-avatar style="background: #79919d"><interaction-outlined style="font-size: 16px" title="未读消息"/></a-avatar>
</a-badge>
<a-avatar v-else style="background: #79919d"><interaction-outlined style="font-size: 16px"/></a-avatar>
</template>
<template v-else-if="item.busType=='bpm'">
<a-badge dot v-if="noRead(item)" class="msg-no-read">
<a-avatar style="background: #79919d"><alert-outlined style="font-size: 16px" title="未读消息"/></a-avatar>
</a-badge>
<a-avatar v-else style="background: #79919d"><alert-outlined style="font-size: 16px"/></a-avatar>
</template>
<template v-else>
<a-badge dot v-if="noRead(item)" class="msg-no-read">
<a-avatar style="background: #79919d"><bell-filled style="font-size: 16px" title="未读消息"/></a-avatar>
</a-badge>
<a-avatar v-else style="background: #79919d"><bell-filled style="font-size: 16px" /></a-avatar>
</template>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</template>
<script>
import { FilterOutlined, CloseOutlined, BellFilled, ExclamationOutlined, MailOutlined,InteractionOutlined, AlertOutlined } from '@ant-design/icons-vue';
import { useSysMessage, useMessageHref } from './useSysMessage';
export default {
name: 'SysMessageList',
components: {
FilterOutlined,
CloseOutlined,
BellFilled,
ExclamationOutlined,
MailOutlined,
InteractionOutlined,
AlertOutlined
},
props:{
star: {
type:Boolean,
default: false
}
},
emits:['close', 'detail'],
setup(props, {emit}){
const { messageList,loadEndStatus,loadingMoreStatus,onLoadMore,noRead, getMsgCategory, searchParams, reset, loadData, updateStarMessage } = useSysMessage();
function reload(params){
let { fromUser, rangeDateKey, rangeDate } = params;
searchParams.fromUser = fromUser||'';
searchParams.rangeDateKey = rangeDateKey||'';
searchParams.rangeDate = rangeDate||[];
if(props.star===true){
searchParams.starFlag = '1'
}else{
searchParams.starFlag = ''
}
reset();
loadData();
}
function clickStar(item){
console.log(item)
updateStarMessage(item);
if(item.starFlag=='1'){
item.starFlag = '0'
}else{
item.starFlag = '1'
}
}
const { goPage } = useMessageHref(emit);
function showMessageDetail(record){
record.readFlag = '1'
goPage(record);
emit('close', record.id)
}
return {
messageList,
loadEndStatus,
loadingMoreStatus,
onLoadMore,
noRead,
getMsgCategory,
reload,
clickStar,
showMessageDetail
}
}
};
</script>
<style scoped lang="less">
.msg-no-read{
:deep(.ant-badge-dot){
top: 5px;
right: 3px;
}
}
:deep(.bpm-cuiban-content) p{
display: inherit;
margin-bottom: 0;
margin-top: 0;
}
/** QQYUN-5688 【样式】鼠标放上去怎么不是手 */
:deep(.ant-rate-disabled){
.ant-rate-star{
cursor: pointer !important;
}
}
</style>

View File

@ -0,0 +1,523 @@
<template>
<BasicModal
:canFullscreen="false"
:draggable="false"
:closable="false"
@register="registerModal"
wrapClassName="sys-msg-modal"
:width="800"
:footer="null"
destroyOnClose
>
<template #title>
<div class="sys-msg-modal-title">
<div class="title"></div>
<div class="ant-tabs-nav-wrap">
<div class="ant-tabs-nav-scroll">
<div class="ant-tabs-nav ant-tabs-nav-animated">
<div>
<div
@click="(e) => handleChangeTab(e, 'all')"
role="tab"
aria-disabled="false"
aria-selected="false"
class="ant-tabs-tab"
:class="{ 'ant-tabs-tab-active': activeKey == 'all' }"
>
全部消息
</div>
<div
@click="(e) => handleChangeTab(e, 'star')"
role="tab"
aria-disabled="false"
aria-selected="true"
class="ant-tabs-tab"
:class="{ 'ant-tabs-tab-active': activeKey == 'star' }"
>
标星消息
</div>
</div>
<div
class="ant-tabs-ink-bar ant-tabs-ink-bar-animated"
:style="{
transform: activeKey == 'all' ? 'translate3d(130px, 0px, 0px)' : 'translate3d(215px, 0px, 0px)',
display: 'block',
width: '88px',
height: '1px'
}"
></div>
</div>
</div>
</div>
<!-- 头部图标 -->
<div class="icon-right">
<div class="icons">
<a-popover placement="bottomRight" :overlayStyle="{ width: '400px' }" trigger="click" v-model:open="showSearch">
<template #content>
<div>
<span class="search-label">回复、提到我的人?</span>
<span style="display: inline-block;">
<div v-if="searchParams.fromUser" class="selected-user">
<span>{{searchParams.realname}}</span>
<span class="clear-user-icon"><close-outlined style="font-size: 12px" @click="clearSearchParamsUser"/></span>
</div>
<a-button v-else type="dashed" shape="circle" @click="openSelectPerson">
<plus-outlined />
</a-button>
</span>
</div>
<div class="search-date">
<div class="date-label">时间:</div>
<div class="date-tags">
<div class="tags-container">
<div v-for="item in dateTags" :class="item.active == true ? 'tag active' : 'tag'" @click="handleClickDateTag(item)">{{
item.text
}}</div>
</div>
<div class="cust-range-date" v-if="showRangeDate">
<a-range-picker v-model:value="searchRangeDate" @change="handleChangeSearchDate" />
</div>
</div>
</div>
</template>
<span v-if="conditionStr" class="anticon filtera">
<filter-outlined />
<span style="font-size:12px;margin-left: 3px">{{conditionStr}}</span>
<span style="display: flex;margin:0 5px;"><close-outlined style="font-size: 12px" @click="clearAll"/></span>
</span>
<filter-outlined v-else/>
</a-popover>
<close-outlined @click="closeModal" />
</div>
</div>
</div>
</template>
<div class="sys-message-card">
<a-tabs :activeKey="activeKey" center @tabClick="handleChangePanel" animated>
<template #renderTabBar>
<div></div>
</template>
<a-tab-pane tab="全部消息" key="all" forceRender>
<sys-message-list ref="allMessageRef" @close="hrefThenClose" @detail="showDetailModal"/>
</a-tab-pane>
<!-- 标星 -->
<a-tab-pane tab="标星消息" key="star" forceRender>
<sys-message-list ref="starMessageRef" star @close="hrefThenClose" @detail="showDetailModal"/>
</a-tab-pane>
</a-tabs>
</div>
</BasicModal>
<user-select-modal isRadioSelection :showButton="false" labelKey="realname" rowKey="username" @register="regModal" @getSelectResult="getSelectedUser"></user-select-modal>
<DetailModal @register="registerDetail" :zIndex="1001"/>
</template>
<script>
import { BasicModal, useModalInner, useModal } from '/@/components/Modal';
import { FilterOutlined, CloseOutlined, BellFilled, ExclamationOutlined, PlusOutlined } from '@ant-design/icons-vue';
import JSelectUser from '/@/components/Form/src/jeecg/components/JSelectUser.vue';
import { ref, unref, reactive, computed } from 'vue';
// import SysMessageList from './SysMessageList.vue'
import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue'
import DetailModal from '/@/views/monitor/mynews/DetailModal.vue';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export default {
name: 'SysMessageModal',
components: {
BasicModal,
FilterOutlined,
CloseOutlined,
BellFilled,
ExclamationOutlined,
JSelectUser,
SysMessageList: createAsyncComponent(() => import('./SysMessageList.vue')),
UserSelectModal,
PlusOutlined,
DetailModal
},
emits:['register', 'refresh'],
setup(_p, {emit}) {
const allMessageRef = ref();
const starMessageRef = ref();
const activeKey = ref('all');
function handleChangeTab(e, key) {
activeKey.value = key;
loadData();
}
function handleChangePanel(key) {
activeKey.value = key;
}
// 查询区域存储值
const searchParams = reactive({
fromUser: '',
realname: '',
rangeDateKey: '7day',
rangeDate: [],
});
function loadData(){
let params = {
fromUser: searchParams.fromUser,
rangeDateKey: searchParams.rangeDateKey,
rangeDate: searchParams.rangeDate,
}
if(activeKey.value == 'all'){
getRefPromise(allMessageRef).then(() => {
allMessageRef.value.reload(params);
});
}else{
starMessageRef.value.reload(params);
}
}
//useModalInner
const [registerModal, { closeModal }] = useModalInner(async (data) => {
//每次弹窗打开 加载最新的数据
loadData();
});
const showSearch = ref(false);
function handleChangeSearchPerson(value, a) {
console.log('选择改变', value, a);
showSearch.value = true;
}
const dateTags = reactive([
{ key: 'jt', text: '今天', active: false },
{ key: 'zt', text: '昨天', active: false },
{ key: 'qt', text: '前天', active: false },
{ key: 'bz', text: '本周', active: false },
{ key: 'sz', text: '上周', active: false },
{ key: 'by', text: '本月', active: false },
{ key: 'sy', text: '上月', active: false },
{ key: '7day', text: '七日', active: true },
{ key: 'zdy', text: '自定义', active: false },
]);
function handleClickDateTag(item) {
for (let a of dateTags) {
if(a.key != item.key){
a.active = false;
}
}
item.active = !item.active;
if(item.active == false){
searchParams.rangeDateKey = ''
}else{
searchParams.rangeDateKey = item.key;
}
if (item.key == 'zdy') {
// 自定义日期查询走的是 handleChangeSearchDate
if(item.active == false){
searchParams.rangeDate = []
loadData();
}
}else{
loadData();
}
}
const showRangeDate = computed(() => {
let temp = dateTags.filter((i) => i.active == true);
if (temp && temp.length > 0) {
if (temp[0].text == '自定义') {
return true;
}
}
return false;
});
const searchRangeDate = ref([]);
function handleChangeSearchDate(_value, dateStringArray) {
searchParams.rangeDate = [...dateStringArray]
loadData();
}
function hrefThenClose(id){
emit('refresh', id)
// closeModal();
}
// 有查询条件值的时候显示该字符串
const conditionStr = computed(()=>{
const { fromUser, rangeDateKey, realname } = searchParams;
if(!fromUser && !rangeDateKey){
return ''
}
let arr = [];
if(fromUser){
arr.push(realname)
}
if(rangeDateKey){
let rangDates = dateTags.filter(item=>item.key == rangeDateKey);
if(rangDates && rangDates.length>0){
arr.push(rangDates[0].text)
}
}
return arr.join('、')
});
//注册model
const [regModal, { openModal }] = useModal();
function getSelectedUser(options, value){
if(options && options.length>0){
searchParams.fromUser = value
searchParams.realname = options[0].label;
}
}
function openSelectPerson(){
openModal(true, {})
}
function clearSearchParamsUser(){
searchParams.fromUser = ''
searchParams.realname = ''
}
function getRefPromise(componentRef) {
return new Promise((resolve) => {
(function next() {
let ref = componentRef.value;
if (ref) {
resolve(ref);
} else {
setTimeout(() => {
next();
}, 100);
}
})();
});
}
function clearAll(){
searchParams.fromUser='';
searchParams.realname='';
searchParams.rangeDateKey='';
searchParams.rangeDate=[];
for (let a of dateTags) {
a.active = false;
}
loadData();
}
const [registerDetail, { openModal: openDetailModal }] = useModal();
function showDetailModal(record){
openDetailModal(true, {record: unref(record), isUpdate: true})
}
return {
conditionStr,
regModal,
getSelectedUser,
openSelectPerson,
clearSearchParamsUser,
clearAll,
registerModal,
activeKey,
handleChangePanel,
handleChangeTab,
showSearch,
searchParams,
handleChangeSearchPerson,
dateTags,
handleClickDateTag,
showRangeDate,
searchRangeDate,
handleChangeSearchDate,
closeModal,
hrefThenClose,
allMessageRef,
starMessageRef,
registerDetail,
showDetailModal
};
},
};
</script>
<style lang="less">
@keyframes move22{
0%{ transform:translateY(0px); }
100%{transform:translateY(600px);}
}
.sys-msg-modal {
.ant-modal-header {
padding-bottom: 0;
padding-top: 6px;
padding-right: 12px;
}
.ant-modal-body {
height: 550px;
overflow-y: auto;
}
.ant-modal {
position: absolute;
right: 10px;
top: calc(100% - 600px);
/* animation-name: move22;
animation-duration:0.8s;
animation-timing-function:ease;*/
}
}
.sys-msg-modal-title {
.title {
display: inline-block;
width: 120px;
}
.icon-right {
display: inline-block;
width: 220px;
vertical-align: top;
.icons {
display: flex;
height: 100%;
flex-direction: row;
justify-content: flex-end;
> span.anticon {
padding: 10px;
display: inline-block;
cursor: pointer;
}
> span.filtera{
//background-color: #d3eafd;
background-color: #eff1f2;
border-radius: 4px;
cursor: pointer;
height: 27px;
padding-top: 7px;
margin-top: 3px;
line-height: 25px;
//color: #2196f3;
display: flex;
>span.anticon{
height: auto;
line-height: 9px;
display: inline-block;
}
}
}
}
.ant-tabs-nav-wrap {
display: inline-block;
width: calc(100% - 340px);
.ant-tabs-tab {
position: relative;
display: inline-flex;
align-items: center;
padding: 12px 0;
font-size: 14px;
background: transparent;
border: 0;
outline: none;
cursor: pointer;
}
.ant-tabs-tab {
position: relative;
display: inline-flex;
align-items: center;
padding: 12px 0;
font-size: 14px;
background: transparent;
border: 0;
outline: none;
cursor: pointer;
}
.ant-tabs-tab+.ant-tabs-tab {
margin: 0 0 0 32px;
}
.ant-tabs-ink-bar {
background: @primary-color;
}
}
.ant-tabs-nav-scroll {
text-align: center;
font-size: 14px;
font-weight: normal;
}
}
.sys-message-card {
}
.search-label {
font-weight: 500;
font-size: 14px !important;
color: #757575 !important;
display: inline-block;
margin-right: 15px !important;
}
.search-date {
display: flex;
min-width: 0;
margin-top: 15px;
.date-label {
margin-top: 4px;
font-weight: 500;
font-size: 14px !important;
color: #757575 !important;
margin-right: 15px !important;
}
.date-tags {
display: -ms-flexbox;
display: flex;
display: -webkit-flex;
-ms-flex-direction: column;
-webkit-flex-direction: column;
flex-direction: column;
-webkit-flex: 1;
flex: 1;
-ms-flex: 1;
.tags-container {
display: flex;
min-width: 0;
-webkit-flex: 1;
flex: 1;
-ms-flex: 1;
flex-wrap: wrap;
.tag {
background-color: #f5f5f5;
border-radius: 17px;
font-size: 13px;
margin-bottom: 10px;
margin-right: 10px;
padding: 6px 12px;
cursor: pointer;
&.active {
background-color: #d3eafd !important;
}
}
}
}
}
.selected-user{
background: #f5f5f5;
padding: 2px 14px;
border-radius: 30px;
.clear-user-icon{
margin-left: 5px;
}
}
</style>

View File

@ -0,0 +1,239 @@
import { ref, reactive } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { getDictItemsByCode } from '/@/utils/dict/index';
import { useRouter, useRoute } from 'vue-router'
import { useAppStore } from '/@/store/modules/app';
import { useTabs } from '/@/hooks/web/useTabs';
import { useModal } from '/@/components/Modal';
import {useMessage} from "/@/hooks/web/useMessage";
/**
* 列表接口
* @param params
*/
const queryMessageList = (params) => {
const url = '/sys/annountCement/vue3List';
return defHttp.get({ url, params });
};
/**
* 获取消息列表数据
*/
export function useSysMessage() {
const { createMessage } = useMessage();
const rangeDateArray = getDictItemsByCode('rangeDate');
console.log('+++++++++++++++++++++');
console.log('rangeDateArray', rangeDateArray);
console.log('+++++++++++++++++++++');
const messageList = ref<any[]>([]);
const pageNo = ref(1)
let pageSize = 10;
const searchParams = reactive({
fromUser: '',
rangeDateKey: '',
rangeDate: [],
starFlag: ''
});
function getQueryParams() {
let { fromUser, rangeDateKey, rangeDate, starFlag } = searchParams;
let params = {
fromUser,
starFlag,
rangeDateKey,
beginDate: '',
endDate: '',
pageNo: pageNo.value,
pageSize
};
if (rangeDateKey == 'zdy') {
params.beginDate = rangeDate[0]+' 00:00:00';
params.endDate = rangeDate[1]+' 23:59:59';
}
return params;
}
// 数据是否加载完了
const loadEndStatus = ref(false);
//请求数据
async function loadData() {
if(loadEndStatus.value === true){
return;
}
let params = getQueryParams();
const data = await queryMessageList(params);
console.log('获取结果', data);
if(!data || data.length<=0){
loadEndStatus.value = true;
return;
}
if(data.length<pageSize){
loadEndStatus.value = true;
}
pageNo.value = pageNo.value+1
let temp:any[] = messageList.value;
temp.push(...data);
messageList.value = temp;
}
//重置
function reset(){
messageList.value = []
pageNo.value = 1;
loadEndStatus.value = false;
}
//标星
async function updateStarMessage(item){
const url = '/sys/sysAnnouncementSend/edit';
let starFlag = '1';
if(item.starFlag==starFlag){
starFlag = '0'
}
const params = {
starFlag,
id: item.sendId
}
//update-begin-author:taoyan date:2023-3-6 for: QQYUN-4491【应用】一些小问题 4、标星不需要提示吧
const data:any = await defHttp.put({url, params}, {isTransformResponse: false});
if(data.success === true){
}else{
createMessage.warning(data.message)
}
//update-end-author:taoyan date:2023-3-6 for: QQYUN-4491【应用】一些小问题 4、标星不需要提示吧
}
const loadingMoreStatus = ref(false);
async function onLoadMore() {
loadingMoreStatus.value = true;
await loadData();
loadingMoreStatus.value = false;
}
function noRead(item) {
if (item.readFlag === '1') {
return false;
}
return true;
}
// 消息类型
function getMsgCategory(item) {
if(item.busType=='email'){
return '邮件提醒:';
} else if(item.busType=='bpm'){
return '流程催办:';
} else if(item.busType=='bpm_cc'){
return '流程抄送:';
}else if(item.busType=='bpm_task'){
return '流程任务:';
} else if (item.msgCategory == '2') {
return '系统消息:';
} else if (item.msgCategory == '1') {
return '通知公告:';
}
return '';
}
return {
messageList,
reset,
loadData,
loadEndStatus,
searchParams,
updateStarMessage,
onLoadMore,
noRead,
getMsgCategory,
};
}
/**
* 用于消息跳转
*/
export function useMessageHref(emit, props){
const messageHrefArray: any[] = getDictItemsByCode('messageHref');
const router = useRouter();
const appStore = useAppStore();
const rt = useRoute();
const { close: closeTab, closeSameRoute } = useTabs();
// const defaultPath = '/monitor/mynews';
//const bpmPath = '/task/handle/'
async function goPage(record, openModalFun?){
if(!record.busType || record.busType == 'msg_node'){
if(!openModalFun){
// 从首页的消息通知跳转
await goPageFromOuter(record);
}else{
// 从消息页面列表点击详情查看 直接打开modal
openModalFun()
}
}else{
await goPageWithBusType(record)
}
/* busId: "1562035005173587970"
busType: "email"
openPage: "modules/eoa/email/modals/EoaEmailInForm"
openType: "component"*/
}
/**
* 根据busType不同跳转不同页面
* @param record
*/
async function goPageWithBusType(record){
const { busType, busId, msgAbstract } = record;
let temp = messageHrefArray.filter(item=>item.value === busType);
if(!temp || temp.length==0){
console.error('当前业务类型不识别', busType);
return;
}
let path = temp[0].text;
path = path.replace('{DETAIL_ID}', busId)
//固定参数 detailId 用于查询表单数据
let query:any = {
detailId: busId
};
// 额外参数处理
if(msgAbstract){
try {
let json = JSON.parse(msgAbstract);
Object.keys(json).map(k=>{
query[k] = json[k]
});
}catch (e) {
console.error('msgAbstract参数不是JSON格式', msgAbstract)
}
}
// 跳转路由
appStore.setMessageHrefParams(query);
if(rt.path.indexOf(path)>=0){
await closeTab();
await router.replace({ path: path, query:{ time: new Date().getTime() } });
}else{
closeSameRoute(path)
await router.push({ path: path });
}
}
/**
* 从首页的消息通知跳转消息列表打开modal
* @param record
*/
async function goPageFromOuter(record){
//没有定义业务类型 直接跳转我的消息页面
emit('detail', record)
}
return {
goPage
}
}

View File

@ -0,0 +1,24 @@
<template>
<BasicDrawer @register="registerModal" title="详情" :width="600" v-bind="$attrs" @ok="closeDrawer">
<BasicForm @register="registerForm" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { BasicForm, useForm } from '/@/components/Form/index';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import { formSchemas } from './manage.data';
// 声明 emits
const emit = defineEmits(['register']);
// 注册 form
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: formSchemas,
showActionButtonGroup: false,
});
// 注册 modal
const [registerModal, { closeDrawer }] = useDrawerInner(async (data) => {
await resetFields();
await setFieldsValue({ ...data.record });
});
</script>

View File

@ -0,0 +1,5 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-message-manage';
.@{prefix-cls} {
}

View File

@ -0,0 +1,129 @@
<template>
<div :class="prefixCls">
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-dropdown v-if="showBatchBtn">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="onDeleteBatch">
<Icon icon="ant-design:delete-outlined"></Icon>
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作</span>
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<ManageDrawer @register="registerDrawer" />
</div>
</template>
<script lang="ts" setup name="message-manage">
import { unref, computed } from 'vue';
import { ActionItem, BasicTable, TableAction } from '/@/components/Table';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import ManageDrawer from './ManageDrawer.vue';
import { Api, list, deleteBatch } from './manage.api';
import { columns, searchFormSchema } from './manage.data';
// 列表页面公共参数、方法
const { prefixCls, tableContext } = useListPage({
designScope: 'message-manage',
tableProps: {
title: '消息中心模板列表数据',
api: list,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
},
exportConfig: {
url: Api.exportXls,
name: '消息中心模板列表',
},
importConfig: {
url: Api.importXls,
success: () => reload(),
},
});
// 注册 ListTable
const [registerTable, { reload, setLoading }, { rowSelection, selectedRowKeys }] = tableContext;
const showBatchBtn = computed(() => selectedRowKeys.value.length > 0);
const [registerDrawer, { openDrawer }] = useDrawer();
function onDetail(record) {
openDrawer(true, { record: record });
}
function onDelete(record) {
if (record) {
doDeleteDepart([record.id], false);
}
}
async function onDeleteBatch() {
try {
await doDeleteDepart(selectedRowKeys);
selectedRowKeys.value = [];
} finally {
}
}
/**
* 根据 ids 批量删除
* @param idListRef array
* @param confirm 是否显示确认提示框
*/
async function doDeleteDepart(idListRef, confirm = true) {
const idList = unref(idListRef);
if (idList.length > 0) {
try {
setLoading(true);
await deleteBatch({ ids: idList.join(',') }, confirm);
await reload();
} finally {
setLoading(false);
}
}
}
/**
* 操作栏
*/
function getTableAction(record): ActionItem[] {
return [{ label: '详情', onClick: onDetail.bind(null, record) }];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record): ActionItem[] {
return [
{
label: '删除',
color: 'error',
popConfirm: {
title: '确认要删除吗?',
confirm: onDelete.bind(null, record),
},
},
];
}
</script>
<style lang="less">
@import 'index';
</style>

View File

@ -0,0 +1,52 @@
import { unref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
export enum Api {
list = '/sys/message/sysMessage/list',
delete = '/sys/message/sysMessage/delete',
deleteBatch = '/sys/message/sysMessage/deleteBatch',
exportXls = 'sys/message/sysMessage/exportXls',
importXls = 'sys/message/sysMessage/importExcel',
save = '/sys/message/sysMessage/add',
edit = '/sys/message/sysMessage/edit',
}
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 批量删除
* @param params
* @param confirm
*/
export const deleteBatch = (params, confirm = false) => {
return new Promise((resolve, reject) => {
const doDelete = () => {
resolve(defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }));
};
if (confirm) {
createConfirm({
iconType: 'warning',
title: '删除',
content: '确定要删除吗?',
onOk: () => doDelete(),
onCancel: () => reject(),
});
} else {
doDelete();
}
});
};
/**
* 保存或者更改消息模板
*/
export const saveOrUpdate = (params, isUpdate) => {
if (unref(isUpdate)) {
return defHttp.put({ url: Api.edit, params });
} else {
return defHttp.post({ url: Api.save, params });
}
};

View File

@ -0,0 +1,134 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '消息标题',
dataIndex: 'esTitle',
width: 140,
},
{
title: '发送内容',
dataIndex: 'esContent',
width: 200,
// slots: { customRender: 'esContent' },
},
{
title: '接收人',
dataIndex: 'esReceiver',
width: 140,
},
{
title: '发送次数',
dataIndex: 'esSendNum',
width: 120,
},
{
title: '发送状态',
dataIndex: 'esSendStatus_dictText',
width: 120,
},
{
title: '发送时间',
dataIndex: 'esSendTime',
width: 140,
},
{
title: '发送方式',
dataIndex: 'esType_dictText',
width: 120,
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '消息标题',
field: 'esTitle',
component: 'Input',
},
{
label: '发送状态',
field: 'esSendStatus',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'msgSendStatus',
},
},
{
label: '发送方式',
field: 'esType',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'messageType',
},
},
];
export const formSchemas: FormSchema[] = [
{
label: 'ID',
field: 'id',
component: 'Input',
show: false,
},
{
label: '消息标题',
field: 'esTitle',
component: 'Input',
componentProps: { readOnly: true },
},
{
label: '发送内容',
field: 'esContent',
component: 'InputTextArea',
componentProps: { readOnly: true },
},
{
label: '发送参数',
field: 'esParam',
component: 'Input',
componentProps: { readOnly: true },
},
{
label: '接收人',
field: 'esReceiver',
component: 'Input',
componentProps: { readOnly: true },
},
{
label: '发送方式',
field: 'esType',
component: 'JDictSelectTag',
componentProps: { disabled: true, dictCode: 'messageType' },
},
{
label: '发送时间',
field: 'esSendTime',
component: 'Input',
componentProps: { readOnly: true },
},
{
label: '发送状态',
field: 'esSendStatus',
component: 'JDictSelectTag',
componentProps: { disabled: true, dictCode: 'msgSendStatus' },
},
{
label: '发送次数',
field: 'esSendNum',
component: 'Input',
componentProps: { readOnly: true },
},
{
label: '发送失败原因',
field: 'esResult',
component: 'Input',
componentProps: { readOnly: true },
},
{
label: '备注',
field: 'remark',
component: 'InputTextArea',
componentProps: { readOnly: true },
},
];

View File

@ -0,0 +1,51 @@
<template>
<BasicModal @register="registerModal" :title="title" :width="800" v-bind="$attrs" @ok="onSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { formSchemas } from './template.data';
import { saveOrUpdate } from './template.api';
// 声明 emits
const emit = defineEmits(['success', 'register']);
const title = ref<string>('');
const isUpdate = ref<boolean>(false);
// 注册 form
//update-begin---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能------------
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema, setProps }] = useForm({
//update-end---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能--------------z
schemas: formSchemas,
showActionButtonGroup: false,
});
// 注册 modal
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({confirmLoading: false,showCancelBtn:!!data?.showFooter,showOkBtn:!!data?.showFooter});
isUpdate.value = unref(data.isUpdate);
title.value = unref(data.title);
await resetFields();
await setFieldsValue({ ...data.record });
// 隐藏底部时禁用整个表单
setProps({ disabled: !data?.showFooter })
});
//表单提交事件
async function onSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
// 提交表单
await saveOrUpdate(values, isUpdate);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,40 @@
<template>
<BasicModal @register="registerModal" title="发送测试" :width="800" v-bind="$attrs" @ok="onSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { sendTestFormSchemas } from './template.data';
import { sendMessageTest } from './template.api';
// 声明 emits
const emit = defineEmits(['register']);
// 注册 form
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: sendTestFormSchemas,
showActionButtonGroup: false,
});
// 注册 modal
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
await resetFields();
await setFieldsValue({ ...unref(data.record) });
});
//表单提交事件
async function onSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
// 提交表单
await sendMessageTest(values);
//关闭弹窗
closeModal();
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,5 @@
//noinspection LessUnresolvedVariable
@prefix-cls: ~'@{namespace}-message-template';
.@{prefix-cls} {
}

View File

@ -0,0 +1,209 @@
<template>
<div :class="prefixCls">
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAdd">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="showBatchBtn">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="onDeleteBatch">
<Icon icon="ant-design:delete-outlined"></Icon>
<span>删除</span>
</a-menu-item>
</a-menu>
</template>
<a-button>
<span>批量操作</span>
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<TemplateModal @register="registerModal" @success="reload" />
<TemplateTestModal @register="registerTestModal" />
</div>
</template>
<script lang="ts" setup name="message-template">
import { unref, computed, toRaw } from 'vue';
import { ActionItem, BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useListPage } from '/@/hooks/system/useListPage';
import TemplateModal from './TemplateModal.vue';
import TemplateTestModal from './TemplateTestModal.vue';
import { Api, saveOrUpdate, list, deleteBatch } from './template.api';
import { columns, searchFormSchema } from './template.data';
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
// 列表页面公共参数、方法
const { prefixCls, onExportXls, onImportXls, tableContext } = useListPage({
designScope: 'message-template',
tableProps: {
title: '消息中心模板列表数据',
api: list,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
},
exportConfig: {
url: Api.exportXls,
name: '消息中心模板列表',
},
importConfig: {
url: Api.importXls,
success: () => reload(),
},
});
// 注册 ListTable
const [registerTable, { reload, setLoading }, { rowSelection, selectedRowKeys, selectedRows }] = tableContext;
const [registerModal, { openModal }] = useModal();
const [registerTestModal, testModal] = useModal();
const showBatchBtn = computed(() => selectedRowKeys.value.length > 0);
function onAdd() {
openModal(true, {
title: '新增消息模板',
isUpdate: false,
showFooter: true,
record: {},
});
}
function onEdit(record) {
if (record.useStatus === '1') {
createMessage.warning('此模板已被应用,禁止编辑!');
return;
}
openModal(true, {
title: '修改消息模板',
isUpdate: true,
record: record,
showFooter: true,
});
}
function onDelete(record) {
if (record) {
//update-begin-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
if(record.useStatus == '1'){
createMessage.warning('该模板已被应用禁止删除!');
return;
}
//update-end-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
doDeleteDepart([record.id], false);
}
}
/**
* 根据 ids 批量删除
* @param idListRef array
* @param confirm 是否显示确认提示框
*/
async function doDeleteDepart(idListRef, confirm = true) {
const idList = unref(idListRef);
if (idList.length > 0) {
try {
setLoading(true);
await deleteBatch({ ids: idList.join(',') }, confirm);
await reload();
} finally {
setLoading(false);
}
}
}
async function onDeleteBatch() {
try {
//update-begin-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
let arr = toRaw(selectedRows.value);
let temp = arr.filter(item=>item.useStatus=='1')
if(temp.length>0){
createMessage.warning('选中的模板已被应用禁止删除!');
return;
}
//update-end-author:taoyan date:2022-7-14 for: VUEN-1652【bug】应用状态下不允许删除
await doDeleteDepart(selectedRowKeys);
selectedRowKeys.value = [];
} finally {
}
}
// 发送消息测试
function onSendTest(record) {
testModal.openModal(true, { record });
}
/**
* 操作栏
*/
function getTableAction(record): ActionItem[] {
//update-begin---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能------------
return [{ label: '查看', onClick: handleDetail.bind(null, record)}, { label: '编辑', onClick: onEdit.bind(null, record) }];
//update-end---author:wangshuai ---date:20221123 for[VUEN-2807]消息模板加一个查看功能------------
}
/**
* 下拉操作栏
*/
function getDropDownAction(record): ActionItem[] {
return [
{ label: '应用', onClick: handleUse.bind(null, record) },
{ label: '停用', onClick: handleNotUse.bind(null, record) },
{ label: '发送测试', onClick: onSendTest.bind(null, record) },
{
label: '删除',
color: 'error',
popConfirm: {
title: '确认要删除吗?',
confirm: onDelete.bind(null, record),
},
},
];
}
/**
* 应用
*/
async function handleUse(record) {
let param = { id: record.id, useStatus: '1' };
await saveOrUpdate(param, true);
await reload();
}
/**
* 停用
*/
async function handleNotUse(record) {
let param = { id: record.id, useStatus: '0' };
await saveOrUpdate(param, true);
await reload();
}
/**
* 查看
* @param record
*/
function handleDetail(record) {
openModal(true,{
title: "消息模板详情",
isUpdate: true,
showFooter: false,
record:record
})
}
</script>
<style lang="less">
@import 'index';
</style>

View File

@ -0,0 +1,60 @@
import { unref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
const { createConfirm } = useMessage();
export enum Api {
list = '/sys/message/sysMessageTemplate/list',
delete = '/sys/message/sysMessageTemplate/delete',
deleteBatch = '/sys/message/sysMessageTemplate/deleteBatch',
exportXls = 'sys/message/sysMessageTemplate/exportXls',
importXls = 'sys/message/sysMessageTemplate/importExcel',
save = '/sys/message/sysMessageTemplate/add',
edit = '/sys/message/sysMessageTemplate/edit',
// 发送测试
send = '/sys/message/sysMessageTemplate/sendMsg',
}
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 批量删除
* @param params
* @param confirm
*/
export const deleteBatch = (params, confirm = false) => {
return new Promise((resolve, reject) => {
const doDelete = () => {
resolve(defHttp.delete({ url: Api.deleteBatch, params }, { joinParamsToUrl: true }));
};
if (confirm) {
createConfirm({
iconType: 'warning',
title: '删除',
content: '确定要删除吗?',
onOk: () => doDelete(),
onCancel: () => reject(),
});
} else {
doDelete();
}
});
};
/**
* 保存或者更改消息模板
*/
export const saveOrUpdate = (params, isUpdate) => {
if (unref(isUpdate)) {
return defHttp.put({ url: Api.edit, params });
} else {
return defHttp.post({ url: Api.save, params });
}
};
/**
* 发送消息测试
* @param params
*/
export const sendMessageTest = (params) => defHttp.post({ url: Api.send, params });

View File

@ -0,0 +1,185 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
import { filterDictTextByCache } from '/@/utils/dict/JDictSelectUtil';
export const columns: BasicColumn[] = [
{
title: '模板标题',
dataIndex: 'templateName',
width: 80,
},
{
title: '模板编码',
dataIndex: 'templateCode',
width: 100,
},
{
title: '通知模板',
dataIndex: 'templateContent',
width: 150,
},
{
title: '模板类型',
dataIndex: 'templateType',
width: 100,
customRender: ({ text }) => filterDictTextByCache('msgType', text),
},
{
title: '是否应用',
dataIndex: 'useStatus',
width: 90,
customRender: function ({ text }) {
if (text == '1') {
return '是';
} else {
return '否';
}
},
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '模板标题',
field: 'templateName',
component: 'Input',
},
{
label: '模板编码',
field: 'templateCode',
component: 'Input',
},
{
label: '模板类型',
field: 'templateType',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'msgType',
},
},
];
export const formSchemas: FormSchema[] = [
{
label: 'ID',
field: 'id',
component: 'Input',
show: false,
},
{
label: '模板标题',
field: 'templateName',
component: 'Input',
required: true,
},
{
label: '模板编码',
field: 'templateCode',
component: 'Input',
dynamicRules: ({ model, schema }) => {
return [ ...rules.duplicateCheckRule('sys_sms_template', 'template_code', model, schema, true)];
},
// 编辑模式下不可修改编码
dynamicDisabled: (params) => !!params.values.id,
},
{
label: '模板类型',
field: 'templateType',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'msgType',
placeholder: '请选择模板类型',
},
required: true,
},
{
label: '是否应用',
field: 'useStatus',
component: 'JSwitch',
componentProps: {
options: ['1', '0'],
},
},
{
label: '模板内容',
field: 'templateContent',
component: 'InputTextArea',
componentProps: {
autoSize: {
minRows: 8,
maxRows: 8,
},
},
ifShow: ({ values }) => {
return !['2', '4', '5'].includes(values.templateType);
},
},
{
label: '模板内容',
field: 'templateContent',
component: 'JEditor',
ifShow: ({ values }) => {
return ['2', '4'].includes(values.templateType);
},
},
{
label: '模板内容',
field: 'templateContent',
component: 'JMarkdownEditor',
ifShow: ({ values }) => {
return ['5'].includes(values.templateType);
},
},
];
export const sendTestFormSchemas: FormSchema[] = [
{
label: '模板编码',
field: 'templateCode',
component: 'Input',
show: false,
},
{
label: '模板标题',
field: 'templateName',
component: 'Input',
componentProps: { disabled: true },
},
{
label: '模板内容',
field: 'templateContent',
component: 'InputTextArea',
componentProps: { disabled: true, rows: 5 },
},
{
label: '测试数据',
field: 'testData',
component: 'InputTextArea',
required: true,
helpMessage: 'JSON数据',
defaultValue: '{}',
componentProps: {
placeholder: '请输入JSON格式测试数据',
rows: 5,
},
},
{
label: '消息类型',
field: 'msgType',
component: 'JDictSelectTag',
required: true,
defaultValue:'system',
componentProps: { dictCode: 'messageType',type:'radio' },
},
{
label: '消息接收方',
field: 'receiver',
required: true,
component: 'JSelectUser',
componentProps: {
labelKey: 'username',
rowKey: 'username',
},
},
];

View File

@ -0,0 +1,24 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" title="查看详情" :showCancelBtn="false" :showOkBtn="false" :maxHeight="500">
<iframe :src="frameSrc" class="detail-iframe" />
</BasicModal>
</template>
<script lang="ts" setup>
import { BasicModal, useModalInner } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
// 获取props
defineProps({
frameSrc: propTypes.string.def(''),
});
//表单赋值
const [registerModal] = useModalInner();
</script>
<style scoped lang="less">
.detail-iframe {
border: 0;
width: 100%;
height: 100%;
min-height: 500px;
}
</style>

View File

@ -0,0 +1,60 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="title" @ok="handleSubmit" width="900px" destroyOnClose>
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './notice.data';
import { saveOrUpdate } from './notice.api';
// 声明Emits
const emit = defineEmits(['register', 'success']);
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
schemas: formSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
if (data.record.userIds) {
data.record.userIds = data.record.userIds.substring(0, data.record.userIds.length - 1);
}
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const title = computed(() => (!unref(isUpdate) ? '新增' : '编辑'));
//表单提交事件
async function handleSubmit(v) {
try {
let values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
//update-begin-author:liusq---date:20230404--for: [issue#429]新增通知公告提交指定用户参数有undefined ---
if(values.msgType==='ALL'){
values.userIds = '';
}else{
values.userIds += ',';
}
//update-end-author:liusq---date:20230404--for: [issue#429]新增通知公告提交指定用户参数有undefined ---
await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,168 @@
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button preIcon="ant-design:plus-outlined" type="primary" @click="handleAdd">新建</a-button>
<!-- <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>-->
<!-- <j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>-->
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon style="fontsize: 12px" icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<NoticeModal @register="registerModal" @success="reload" />
<DetailModal @register="register" :frameSrc="iframeUrl" />
</div>
</template>
<script lang="ts" name="system-notice" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import NoticeModal from './NoticeModal.vue';
import DetailModal from './DetailModal.vue';
import { useMethods } from '/@/hooks/system/useMethods';
import { useGlobSetting } from '/@/hooks/setting';
import { getToken } from '/@/utils/auth';
import { columns, searchFormSchema } from './notice.data';
import { getList, deleteNotice, batchDeleteNotice, getExportUrl, getImportUrl, doReleaseData, doReovkeData } from './notice.api';
import { useListPage } from '/@/hooks/system/useListPage';
const glob = useGlobSetting();
const [registerModal, { openModal }] = useModal();
const [register, { openModal: openDetail }] = useModal();
const iframeUrl = ref('');
// 列表页面公共参数、方法
const { prefixCls, onExportXls, onImportXls, tableContext, doRequest } = useListPage({
designScope: 'notice-template',
tableProps: {
title: '消息通知',
api: getList,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
},
exportConfig: {
name: '消息通知列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteNotice({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
doRequest(() => batchDeleteNotice({ ids: selectedRowKeys.value }));
}
/**
* 发布
*/
async function handleRelease(id) {
await doReleaseData({ id });
reload();
}
/**
* 撤销
*/
async function handleReovke(id) {
await doReovkeData({ id });
reload();
}
/**
* 查看
*/
function handleDetail(record) {
iframeUrl.value = `${glob.uploadUrl}/sys/annountCement/show/${record.id}?token=${getToken()}`;
openDetail(true);
}
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: record.sendStatus == 0,
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '删除',
ifShow: record.sendStatus != 1,
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
{
label: '发布',
ifShow: record.sendStatus == 0,
onClick: handleRelease.bind(null, record.id),
},
{
label: '撤销',
ifShow: record.sendStatus == 1,
popConfirm: {
title: '确定要撤销吗',
confirm: handleReovke.bind(null, record.id),
},
},
{
label: '查看',
onClick: handleDetail.bind(null, record),
},
];
}
</script>

View File

@ -0,0 +1,65 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/sys/annountCement/list',
save = '/sys/annountCement/add',
edit = '/sys/annountCement/edit',
delete = '/sys/annountCement/delete',
deleteBatch = '/sys/annountCement/deleteBatch',
exportXls = '/sys/annountCement/exportXls',
importExcel = '/sys/annountCement/importExcel',
releaseData = '/sys/annountCement/doReleaseData',
reovkeData = '/sys/annountCement/doReovkeData',
}
/**
* 导出url
*/
export const getExportUrl = Api.exportXls;
/**
* 导入url
*/
export const getImportUrl = Api.importExcel;
/**
* 查询租户列表
* @param params
*/
export const getList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 保存或者更新通告
* @param params
*/
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 删除通告
* @param params
*/
export const deleteNotice = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量消息公告
* @param params
*/
export const batchDeleteNotice = (params) => defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true });
/**
* 发布
* @param id
*/
export const doReleaseData = (params) => defHttp.get({ url: Api.releaseData, params });
/**
* 撤销
* @param id
*/
export const doReovkeData = (params) => defHttp.get({ url: Api.reovkeData, params });

View File

@ -0,0 +1,156 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
import { render } from '/@/utils/common/renderUtils';
export const columns: BasicColumn[] = [
{
title: '标题',
width: 150,
dataIndex: 'titile',
},
{
title: '消息类型',
dataIndex: 'msgCategory',
width: 100,
customRender: ({ text }) => {
return render.renderDict(text, 'msg_category');
},
},
{
title: '发布人',
width: 100,
dataIndex: 'sender',
},
{
title: '优先级',
dataIndex: 'priority',
width: 70,
customRender: ({ text }) => {
const color = text == 'L' ? 'blue' : text == 'M' ? 'yellow' : 'red';
return render.renderTag(render.renderDict(text, 'priority'), color);
},
},
{
title: '通告对象',
dataIndex: 'msgType',
width: 100,
customRender: ({ text }) => {
return render.renderDict(text, 'msg_type');
},
},
{
title: '发布状态',
dataIndex: 'sendStatus',
width: 70,
customRender: ({ text }) => {
const color = text == '0' ? 'red' : text == '1' ? 'green' : 'gray';
return render.renderTag(render.renderDict(text, 'send_status'), color);
},
},
{
title: '发布时间',
width: 100,
dataIndex: 'sendTime',
},
{
title: '撤销时间',
width: 100,
dataIndex: 'cancelTime',
},
];
export const searchFormSchema: FormSchema[] = [
{
field: 'titile',
label: '标题',
component: 'JInput',
colProps: { span: 8 },
},
];
export const formSchema: FormSchema[] = [
{
field: 'id',
label: 'id',
component: 'Input',
show: false,
},
{
field: 'msgCategory',
label: '消息类型',
required: true,
component: 'JDictSelectTag',
defaultValue: '1',
componentProps: {
type: 'radio',
dictCode: 'msg_category',
placeholder: '请选择类型',
},
},
{
field: 'titile',
label: '标题',
component: 'Input',
required: true,
componentProps: {
placeholder: '请输入标题',
},
},
{
field: 'msgAbstract',
label: '摘要',
component: 'InputTextArea',
required: true,
},
// {
// field: 'endTime',
// label: '截至日期',
// component: 'DatePicker',
// componentProps: {
// showTime: true,
// valueFormat: 'YYYY-MM-DD HH:mm:ss',
// placeholder: '请选择截至日期',
// },
// dynamicRules: ({ model }) => rules.endTime(model.startTime, true),
// },
{
field: 'msgType',
label: '接收用户',
defaultValue: 'ALL',
component: 'JDictSelectTag',
required: true,
componentProps: {
type: 'radio',
dictCode: 'msg_type',
placeholder: '请选择发布范围',
},
},
{
field: 'userIds',
label: '指定用户',
component: 'JSelectUser',
required: true,
componentProps: {
rowKey: 'id',
labelKey: 'username',
},
ifShow: ({ values }) => values.msgType == 'USER',
},
{
field: 'priority',
label: '优先级',
defaultValue: 'H',
component: 'JDictSelectTag',
componentProps: {
dictCode: 'priority',
type: 'radio',
placeholder: '请选择优先级',
},
},
{
field: 'msgContent',
label: '内容',
component: 'Input',
render: render.renderTinymce,
},
];

View File

@ -0,0 +1,20 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/sys/online/list',
forceLogout = '/sys/online/forceLogout'
}
/**
* 列表
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 批量删除角色
* @param params
*/
export const forceLogout = (params) => {
return defHttp.post({url:Api.forceLogout,params},{isTransformResponse:false})
};

View File

@ -0,0 +1,54 @@
import { FormSchema } from '/@/components/Table';
import { render } from "/@/utils/common/renderUtils";
import { getToken } from '/@/utils/auth';
//列表
export const columns = [
{
title:'用户账号',
align:"center",
dataIndex: 'username',
customRender: ( {text,record} ) => {
let token = getToken();
if(record.token === token) {
return text + '(我)'
}
return text
},
},{
title:'用户姓名',
align:"center",
dataIndex: 'realname'
},{
title: '头像',
align: "center",
width: 120,
dataIndex: 'avatar',
customRender: render.renderAvatar,
},{
title:'生日',
align:"center",
dataIndex: 'birthday'
},{
title: '性别',
align: "center",
dataIndex: 'sex',
customRender: ({text}) => {
return render.renderDict(text, 'sex');
}
},{
title:'手机号',
align:"center",
dataIndex: 'phone'
}
];
//查询区域
export const searchFormSchema: FormSchema[] = [
{
field: 'username',
label: '用户账号',
component: 'Input',
colProps: { span: 6 },
}
];

View File

@ -0,0 +1,67 @@
<template>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
</template>
<script lang="ts" name="online-user" setup>
import { BasicTable, TableAction } from '/@/components/Table';
import { columns, searchFormSchema } from './OnlineUser.data';
import { list, forceLogout } from './OnlineUser.api';
import { useListPage } from '/@/hooks/system/useListPage';
import { useMessage } from "/@/hooks/web/useMessage";
// 列表页面公共参数、方法
const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
designScope: 'online-user',
tableProps: {
//在线用户rowKey默认id会造成key重复导致页面出现重复数据
rowKey:'token',
title: '在线用户',
api: list,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
actionColumn: {
width: 120,
},
rowSelection: null,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
const $message = useMessage();
//操作栏
function getTableAction(record) {
return [
{
label: '强退',
popConfirm: {
title: '强制退出用户',
confirm: handleForce.bind(null, record),
},
},
];
}
/**
* 强退
* @param record
*/
function handleForce(record) {
forceLogout({ token: record.token }).then((res)=>{
if(res.success){
reload();
$message.createMessage.success('强制退出用户'+record.realname+'成功');
}else{
$message.createMessage.warn(res.message);
}
})
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,159 @@
<template>
<div>
<!--引用表格-->
<BasicTable @register="registerTable">
<!--插槽:table标题-->
<template #tableTitle>
<a-upload name="file" :showUploadList="false" :action="ossAction" :headers="tokenHeader" :beforeUpload="beforeUpload" @change="handleChange">
<a-button type="primary" preIcon="ant-design:upload-outlined">OSS文件上传</a-button>
</a-upload>
<a-upload
name="file"
:showUploadList="false"
:action="minioAction"
:headers="tokenHeader"
:beforeUpload="beforeUpload"
@change="handleChange"
>
<a-button type="primary" preIcon="ant-design:upload-outlined">MINIO文件上传</a-button>
</a-upload>
</template>
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
</div>
</template>
<script lang="ts" name="system-ossfile" setup>
//ts语法
import { ref, computed, unref } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useMessage } from '/@/hooks/web/useMessage';
import { columns, searchFormSchema } from './ossfile.data';
import { list, deleteFile, getOssUrl, getMinioUrl } from './ossfile.api';
import { useGlobSetting } from '/@/hooks/setting';
import { getToken } from '/@/utils/auth';
import {encryptByBase64} from "@/utils/cipher";
const { createMessage } = useMessage();
const glob = useGlobSetting();
const tokenHeader = { 'X-Access-Token': getToken() };
//注册table数据
const [registerTable, { reload }] = useTable({
api: list,
rowKey: 'id',
columns,
formConfig: {
labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
},
striped: true,
useSearchForm: true,
showTableSetting: true,
clickToRowSelect: false,
bordered: true,
showIndexColumn: false,
tableSetting: { fullScreen: true },
beforeFetch: (params) => {
return Object.assign({ column: 'createTime', order: 'desc' }, params);
},
actionColumn: {
width: 80,
title: '操作',
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
/**
* 上传url
*/
const ossAction = computed(() => `${glob.uploadUrl}${getOssUrl}`);
const minioAction = computed(() => `${glob.uploadUrl}${getMinioUrl}`);
/**
* 预览
*/
function handleView(record) {
if (record && record.url) {
console.log('glob.onlineUrl', glob.viewUrl);
//update-begin---author:scott ---date:2024-06-03 for【TV360X-952】升级到kkfileview4.1.0---
// let filePath = encodeURIComponent(record.url);
let url = encodeURIComponent(encryptByBase64(record.url));
// //文档采用pdf预览高级模式
// if(filePath.endsWith(".pdf") || filePath.endsWith(".doc") || filePath.endsWith(".docx")){
// filePath = filePath
// }
let previewUrl = `${glob.viewUrl}?url=` + url;
//update-end---author:scott ---date:2024-06-03 for【TV360X-952】升级到kkfileview4.1.0---
window.open(previewUrl, '_blank');
}
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteFile({ id: record.id }, reload);
}
/**
* 上传前事件
*/
function beforeUpload(file) {
var fileType = file.type;
if (fileType === 'image') {
if (fileType.indexOf('image') < 0) {
createMessage.warning('请上传图片');
return false;
}
} else if (fileType === 'file') {
if (fileType.indexOf('image') >= 0) {
createMessage.warning('请上传文件');
return false;
}
}
return true;
}
/**
* 文件上传事件
*/
function handleChange(info) {
if (info.file.status === 'done') {
if (info.file.response.success) {
reload();
createMessage.success(`${info.file.name} 上传成功!`);
} else {
createMessage.error(`${info.file.response.message}`);
}
} else if (info.file.status === 'error') {
createMessage.error(`${info.file.response.message}`);
}
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '预览',
onClick: handleView.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
</script>
<style scoped></style>

View File

@ -0,0 +1,33 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/sys/oss/file/list',
deleteFile = '/sys/oss/file/delete',
ossUpload = '/sys/oss/file/upload',
minioUpload = '/sys/upload/uploadMinio',
}
/**
* oss上传
* @param params
*/
export const getOssUrl = Api.ossUpload;
/**
* minio上传
* @param params
*/
export const getMinioUrl = Api.minioUpload;
/**
* 列表接口
* @param params
*/
export const list = (params) => defHttp.get({ url: Api.list, params });
/**
* 删除用户
*/
export const deleteFile = (params, handleSuccess) => {
return defHttp.delete({ url: Api.deleteFile, params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};

View File

@ -0,0 +1,30 @@
import { BasicColumn } from '/@/components/Table';
import { FormSchema } from '/@/components/Table';
export const columns: BasicColumn[] = [
{
title: '文件名称',
dataIndex: 'fileName',
width: 120,
},
{
title: '文件地址',
dataIndex: 'url',
width: 100,
},
];
export const searchFormSchema: FormSchema[] = [
{
label: '文件名称',
field: 'fileName',
component: 'Input',
colProps: { span: 6 },
},
{
label: '文件地址',
field: 'url',
component: 'Input',
colProps: { span: 6 },
},
];

View File

@ -0,0 +1,53 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit" :width="700">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup>
import { ref, computed, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicForm, useForm } from '/@/components/Form/index';
import { formSchema } from './position.data';
import { saveOrUpdatePosition, getPositionById } from './position.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
const isUpdate = ref(true);
//表单配置
const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
//labelWidth: 150,
schemas: formSchema,
showActionButtonGroup: false,
});
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
//重置表单
await resetFields();
setModalProps({ confirmLoading: false });
isUpdate.value = !!data?.isUpdate;
if (unref(isUpdate)) {
//获取详情
data.record = await getPositionById({ id: data.record.id });
//表单赋值
await setFieldsValue({
...data.record,
});
}
});
//设置标题
const getTitle = computed(() => (!unref(isUpdate) ? '新增职务' : '编辑职务'));
//表单提交事件
async function handleSubmit() {
try {
const values = await validate();
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdatePosition(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
} finally {
setModalProps({ confirmLoading: false });
}
}
</script>

View File

@ -0,0 +1,125 @@
<template>
<div>
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd">新增</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="ant-design:down-outlined"></Icon>
</a-button>
</a-dropdown>
</template>
<template #action="{ record }">
<TableAction :actions="getActions(record)" />
</template>
</BasicTable>
<PositionModal @register="registerModal" @success="reload" />
</div>
</template>
<script lang="ts" name="system-position" setup>
import { ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { getPositionList, deletePosition, batchDeletePosition, customUpload, getExportUrl, getImportUrl } from './position.api';
import { columns, searchFormSchema } from './position.data';
import PositionModal from './PositionModal.vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useListPage } from '/@/hooks/system/useListPage';
const { createMessage } = useMessage();
const [registerModal, { openModal }] = useModal();
// 列表页面公共参数、方法
const { prefixCls, onExportXls, onImportXls, tableContext } = useListPage({
designScope: 'position-template',
tableProps: {
title: '职务列表',
api: getPositionList,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
actionColumn: {
width: 180,
},
showIndexColumn: true,
},
exportConfig: {
name: '职务列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 操作列定义
* @param record
*/
function getActions(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
});
}
/**
* 编辑事件
*/
function handleEdit(record) {
openModal(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deletePosition({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeletePosition({ ids: selectedRowKeys.value }, () => {
// update-begin--author:liaozhiyang---date:20240223---for【QQYUN-8334】批量删除之后按钮未隐藏选中记录还在
selectedRowKeys.value = [];
reload();
// update-end--author:liaozhiyang---date:20240223---for【QQYUN-8334】批量删除之后按钮未隐藏选中记录还在
});
}
</script>

View File

@ -0,0 +1,79 @@
import { defHttp } from '/@/utils/http/axios';
import { Modal } from 'ant-design-vue';
enum Api {
list = '/sys/position/list',
save = '/sys/position/add',
edit = '/sys/position/edit',
get = '/sys/position/queryById',
delete = '/sys/position/delete',
importExcel = '/sys/position/importExcel',
exportXls = '/sys/position/exportXls',
deleteBatch = '/sys/position/deleteBatch',
}
/**
* 导出api
*/
export const getExportUrl = Api.exportXls;
export const getImportUrl = Api.importExcel;
/**
* 查询列表
* @param params
*/
export const getPositionList = (params) => {
return defHttp.get({ url: Api.list, params });
};
/**
* 保存或者更新
* @param params
*/
export const saveOrUpdatePosition = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url: url, params });
};
/**
* 查询详情
* @param params
*/
export const getPositionById = (params) => {
return defHttp.get({ url: Api.get, params });
};
/**
* 单条删除
* @param params
*/
export const deletePosition = (params, handleSuccess) => {
return defHttp.delete({ url: Api.delete, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
};
/**
* 批量删除
* @param params
*/
export const batchDeletePosition = (params, handleSuccess) => {
Modal.confirm({
title: '确认删除',
content: '是否删除选中数据',
okText: '确认',
cancelText: '取消',
onOk: () => {
return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
handleSuccess();
});
},
});
};
/**
* 自定义上传
* @param customUpload
*/
export const customUpload = (params) => {
defHttp.uploadFile({ url: Api.importExcel }, params);
};

View File

@ -0,0 +1,71 @@
import { BasicColumn, FormSchema } from '/@/components/Table';
import { rules } from '/@/utils/helper/validator';
export const columns: BasicColumn[] = [
// {
// title: '职务编码',
// dataIndex: 'code',
// width: 200,
// align: 'left',
// },
{
title: '职务名称',
dataIndex: 'name',
align: 'left'
// width: 200,
},
// {
// title: '职务等级',
// dataIndex: 'postRank_dictText',
// width: 100,
// },
];
export const searchFormSchema: FormSchema[] = [
{
field: 'name',
label: '职务名称',
component: 'Input',
colProps: { span: 8 },
},
];
export const formSchema: FormSchema[] = [
{
label: '主键',
field: 'id',
component: 'Input',
show: false,
},
// {
// label: '职级',
// field: 'postRank',
// component: 'JDictSelectTag',
// required: true,
// componentProps: {
// dictCode: 'position_rank',
// dropdownStyle: {
// maxHeight: '100vh',
// },
// getPopupContainer: () => document.body,
// },
// },
{
field: 'name',
label: '职务名称',
component: 'Input',
required: true,
},
// {
// field: 'code',
// label: '职务编码',
// component: 'Input',
// required: true,
// dynamicDisabled: ({ values }) => {
// return !!values.id;
// },
// dynamicRules: ({ model, schema }) => {
// return rules.duplicateCheckRule('sys_position', 'code', model, schema, true);
// },
// },
];

View File

@ -0,0 +1,184 @@
<template>
<BasicTable @register="registerTable">
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleCreate"> 新增</a-button>
<a-dropdown v-if="selectedRowKeys.length > 0">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete">
<Icon icon="ant-design:delete-outlined"></Icon>
删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<Icon icon="mdi:chevron-down"></Icon>
</a-button>
</a-dropdown>
<div style="margin-left: 10px;margin-top: 5px">当前登录租户: <span class="tenant-name">{{loginTenantName}}</span> </div>
</template>
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
</BasicTable>
<!--角色用户表格-->
<RoleUserTable @register="roleUserDrawer" :disableUserEdit="true"/>
<!--角色编辑抽屉-->
<RoleDrawer @register="registerDrawer" @success="reload" :showFooter="showFooter" />
<!--角色详情-->
<RoleDesc @register="registerDesc"></RoleDesc>
</template>
<script lang="ts" name="tenant-role-list" setup>
import { onMounted, ref } from 'vue';
import { BasicTable, TableAction } from '/@/components/Table';
import { useDrawer } from '/@/components/Drawer';
import { useModal } from '/@/components/Modal';
import RoleDesc from './components/RoleDesc.vue';
import RoleDrawer from './components/RoleDrawer.vue';
import RoleUserTable from './components/RoleUserTable.vue';
import { columns, searchFormSchema } from './role.data';
import { listByTenant, deleteRole, batchDeleteRole, getExportUrl, getImportUrl } from './role.api';
import { useListPage } from '/@/hooks/system/useListPage';
import { getLoginTenantName } from "/@/views/system/tenant/tenant.api";
import { tenantSaasMessage } from "@/utils/common/compUtils";
const showFooter = ref(true);
const [roleUserDrawer, { openDrawer: openRoleUserDrawer }] = useDrawer();
const [registerDrawer, { openDrawer }] = useDrawer();
const [registerModal, { openModal }] = useModal();
const [registerDesc, { openDrawer: openRoleDesc }] = useDrawer();
// 列表页面公共参数、方法
const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
designScope: 'role-template',
tableProps: {
title: '租户角色列表',
api: listByTenant,
columns: columns,
formConfig: {
schemas: searchFormSchema,
},
actionColumn: {
width: 120,
},
rowSelection: null,
//自定义默认排序
defSort: {
column: 'id',
order: 'desc',
},
},
exportConfig: {
name: '角色列表',
url: getExportUrl,
},
importConfig: {
url: getImportUrl,
},
});
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
/**
* 新增
*/
function handleCreate() {
showFooter.value = true;
openDrawer(true, {
isUpdate: false,
});
}
/**
* 编辑
*/
function handleEdit(record: Recordable) {
showFooter.value = true;
openDrawer(true, {
record,
isUpdate: true,
});
}
/**
* 详情
*/
function handleDetail(record) {
showFooter.value = false;
openRoleDesc(true, {
record,
isUpdate: true,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteRole({ id: record.id }, reload);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDeleteRole({ ids: selectedRowKeys.value }, reload);
}
/**
* 角色用户
*/
function handleUser(record) {
//onSelectChange(selectedRowKeys)
openRoleUserDrawer(true, record);
}
/**
* 操作栏
*/
function getTableAction(record) {
return [
{
label: '用户',
onClick: handleUser.bind(null, record),
},
];
}
/**
* 下拉操作栏
*/
function getDropDownAction(record) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '详情',
onClick: handleDetail.bind(null, record),
},
{
label: '删除',
popConfirm: {
title: '是否确认删除',
confirm: handleDelete.bind(null, record),
},
},
];
}
const loginTenantName = ref<string>('');
getTenantName();
async function getTenantName(){
loginTenantName.value = await getLoginTenantName();
}
onMounted(()=>{
tenantSaasMessage('租户角色')
})
</script>
<style scoped lang="less">
.tenant-name{
text-decoration:underline;
margin: 5px;
font-size: 15px;
}
</style>

View File

@ -0,0 +1,85 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" title="数据规则配置" width="450px" destroyOnClose>
<a-tabs defaultActiveKey="1">
<a-tab-pane tab="数据规则" key="1">
<a-checkbox-group v-model:value="dataRuleChecked" v-if="dataRuleList.length > 0">
<a-row>
<a-col :span="24" v-for="(item, index) in dataRuleList" :key="'dr' + index">
<a-checkbox :value="item.id">{{ item.ruleName }}</a-checkbox>
</a-col>
<a-col :span="24">
<div style="width: 100%; margin-top: 15px">
<a-button @click="saveDataRuleForRole" type="primary" size="small"> <Icon icon="ant-design:save-outlined"></Icon>点击保存</a-button>
</div>
</a-col>
</a-row>
</a-checkbox-group>
<div v-else><h3>无配置信息!</h3></div>
</a-tab-pane>
</a-tabs>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, unref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
import { useMessage } from '/src/hooks/web/useMessage';
import { queryDataRule, saveDataRule } from '../role.api';
// 声明Emits
const emit = defineEmits(['success', 'register']);
const { createMessage } = useMessage();
// 声明数据
const functionId = ref('');
const roleId = ref('');
const dataRuleList = ref([]);
const dataRuleChecked = ref([]);
/**
* 数据
*/
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
await reset();
setDrawerProps({ confirmLoading: false });
//权限的id
functionId.value = data.functionId;
//角色的id
roleId.value = data.roleId;
//查询数据
const res = await queryDataRule({ functionId: unref(functionId), roleId: unref(roleId) });
if (res.success) {
dataRuleList.value = res.result.datarule;
if (res.result.drChecked) {
dataRuleChecked.value = res.result.drChecked.split(',');
}
}
});
/**
* 重置
*/
function reset() {
functionId.value = '';
roleId.value = '';
dataRuleList.value = [];
dataRuleChecked.value = [];
}
/**
* 提交
*/
async function saveDataRuleForRole() {
if (!unref(dataRuleChecked) || unref(dataRuleChecked).length == 0) {
createMessage.warning('请注意现未勾选任何数据权限!');
}
let params = {
permissionId: unref(functionId),
roleId: unref(roleId),
dataRuleIds: unref(dataRuleChecked).join(','),
};
await saveDataRule(params);
//关闭弹窗
closeDrawer();
//刷新列表
emit('success');
}
</script>

View File

@ -0,0 +1,18 @@
<template>
<BasicDrawer v-bind="$attrs" @register="registerDrawer" title="角色详情" width="500px" destroyOnClose>
<Description :column="1" :data="roleData" :schema="formDescSchema" />
</BasicDrawer>
</template>
<script lang="ts" setup>
import { ref, useAttrs } from 'vue';
import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
import { formDescSchema } from '../role.data';
import { Description, useDescription } from '/@/components/Description/index';
const emit = defineEmits(['register']);
const attrs = useAttrs();
const roleData = ref({});
const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
setDrawerProps({ confirmLoading: false });
roleData.value = data.record;
});
</script>

Some files were not shown because too many files have changed in this diff Show More