feat(消息管理): useSelect钩子&消息管理部分页面&部分组件调整
This commit is contained in:
parent
9404db0222
commit
bbccbe49be
|
@ -570,6 +570,194 @@ const fileList = [
|
|||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 100001,
|
||||
name: 'JAR',
|
||||
url: 'https://github.com/metersphere/metersphere/blob/v2.10/.gitignore',
|
||||
type: 'JAR',
|
||||
desc: 'fwihflhlofihlasjkhfdlkasjdhgaksuidhoasidoasidasopidapsoidaps',
|
||||
storage: 'minio',
|
||||
tag: ['dsadasd'],
|
||||
size: '12MB',
|
||||
enable: true,
|
||||
fileVersion: 'v2.10',
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000002,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000003,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000004,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000005,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000006,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000007,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000008,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000009,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
{
|
||||
id: 1000010,
|
||||
name: 'PNG',
|
||||
url: 'http://localhost:5173/front/base-display/get/logo-platform',
|
||||
type: 'PNG',
|
||||
desc: 'sfakjdghkjrugheoirugkblasjblsdjkhflksdjfsldkjklnjkhkljds',
|
||||
storage: 'github',
|
||||
gitBranch: 'master',
|
||||
gitVersion: 'v2.10',
|
||||
gitPath: '/asdas/xas/xas/fd/f/',
|
||||
tag: ['asfasdfas'],
|
||||
size: '12MB',
|
||||
enable: false,
|
||||
fileModule: 'XXX',
|
||||
creator: '创建人',
|
||||
updater: '更新人',
|
||||
updateTime: 18975439859,
|
||||
createTime: 18975439859,
|
||||
},
|
||||
];
|
||||
// 获取文件列表
|
||||
export function getFileList(data: TableQueryParams): Promise<CommonList<any>> {
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import MSR from '@/api/http/index';
|
||||
import {
|
||||
RobotListUrl,
|
||||
GetRobotUrl,
|
||||
AddRobotUrl,
|
||||
UpdateRobotUrl,
|
||||
EnableRobotUrl,
|
||||
} from '@/api/requrls/project-management/messageManagement';
|
||||
|
||||
import type { RobotItem, RobotAddParams, RobotEditParams } from '@/models/projectManagement/message';
|
||||
import type { TableQueryParams, CommonList } from '@/models/common';
|
||||
|
||||
const list = [
|
||||
{
|
||||
id: '1',
|
||||
name: '站内信',
|
||||
description: '系统内置,在顶部导航栏显示消息通知',
|
||||
platform: 'IN_SITE',
|
||||
enable: true,
|
||||
webhook: 'asdasdasfasfsaf',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: '邮件',
|
||||
description: '系统内置,以添加用户邮箱为通知方式',
|
||||
platform: 'MAIL',
|
||||
enable: false,
|
||||
webhook: 'sdfsdfasfasf',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '飞书',
|
||||
description: '',
|
||||
platform: 'LARK',
|
||||
enable: false,
|
||||
webhook: 'asdfgasdgasfgasgas',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
updateUser: 'bai',
|
||||
updateTime: 1695721467045,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: '钉钉',
|
||||
description: '',
|
||||
platform: 'DING_TALK',
|
||||
enable: false,
|
||||
webhook: 'asfgasfasdfa',
|
||||
type: 'CUSTOM',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
updateUser: 'bai',
|
||||
updateTime: 1695721467045,
|
||||
},
|
||||
{
|
||||
id: '44',
|
||||
name: '钉钉',
|
||||
description: '',
|
||||
platform: 'DING_TALK',
|
||||
enable: false,
|
||||
webhook: 'asfgasfasdfa',
|
||||
appKey: 'asfasfasfasfasf',
|
||||
appSecret: 'asfasfasfasfasf',
|
||||
type: 'ENTERPRISE',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
updateUser: 'bai',
|
||||
updateTime: 1695721467045,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '企业微信',
|
||||
description: '',
|
||||
platform: 'WE_COM',
|
||||
enable: false,
|
||||
webhook: 'vevbbt',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
updateUser: 'bai',
|
||||
updateTime: 1695721467045,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '自定义',
|
||||
description: '',
|
||||
platform: 'CUSTOM',
|
||||
enable: false,
|
||||
webhook: 'bytnm',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
updateUser: 'bai',
|
||||
updateTime: 1695721467045,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '自定义',
|
||||
description: '',
|
||||
platform: 'CUSTOM',
|
||||
enable: false,
|
||||
webhook: 'bytnm',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
updateUser: 'bai',
|
||||
updateTime: 1695721467045,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: '自定义',
|
||||
description: '',
|
||||
platform: 'CUSTOM',
|
||||
enable: false,
|
||||
webhook: 'bytnm',
|
||||
projectId: '1',
|
||||
createTime: 1695721467045,
|
||||
createUser: 'admin',
|
||||
updateUser: 'bai',
|
||||
updateTime: 1695721467045,
|
||||
},
|
||||
];
|
||||
|
||||
export function getRobotList(projectId: string) {
|
||||
// return MSR.post<RobotItem[]>({ url: `${RobotListUrl}/${projectId}` });
|
||||
return Promise.resolve(list);
|
||||
}
|
||||
|
||||
export function getRobotDetail(robotId: string) {
|
||||
// return MSR.get<RobotItem>({ url: GetRobotUrl, params: robotId });
|
||||
return Promise.resolve(list.find((item) => item.id === robotId));
|
||||
}
|
||||
|
||||
export function addRobot(data: RobotAddParams) {
|
||||
return MSR.post({ url: AddRobotUrl, data });
|
||||
}
|
||||
|
||||
export function updateRobot(data: RobotEditParams) {
|
||||
return MSR.post({ url: UpdateRobotUrl, data });
|
||||
}
|
||||
|
||||
export function toggleRobot(id: string) {
|
||||
return MSR.get({ url: EnableRobotUrl, params: id });
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export const RobotListUrl = '/project/robot/list/page';
|
||||
export const UpdateRobotUrl = '/project/robot/update';
|
||||
export const AddRobotUrl = '/project/robot/add';
|
||||
export const GetRobotUrl = '/project/robot/get';
|
||||
export const EnableRobotUrl = '/project/robot/enable';
|
||||
export const DeleteRobotUrl = '/project/robot/delete';
|
|
@ -328,6 +328,16 @@
|
|||
.arco-checkbox-icon {
|
||||
border: 1px solid var(--color-text-input-border);
|
||||
}
|
||||
&:hover {
|
||||
.arco-checkbox-icon-hover::before {
|
||||
background-color: rgb(var(--primary-9)) !important;
|
||||
}
|
||||
}
|
||||
.arco-checkbox-icon-hover {
|
||||
&:hover::before {
|
||||
background-color: rgb(var(--primary-9)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.arco-checkbox-indeterminate .arco-checkbox-icon {
|
||||
border-color: rgba(var(--primary-7));
|
||||
|
@ -407,8 +417,12 @@
|
|||
.arco-dropdown,
|
||||
.arco-trigger-menu,
|
||||
.arco-select-dropdown {
|
||||
padding: 6px;
|
||||
border: 0.5px solid var(--color-text-n8);
|
||||
box-shadow: 0 3px 14px 2px rgb(0 0 0 / 5%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 5px 5px -3px rgb(0 0 0 / 10%);
|
||||
.arco-select-dropdown-header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.arco-dropdown-list,
|
||||
.arco-select-dropdown-list,
|
||||
.arco-trigger-menu-inner {
|
||||
|
@ -427,6 +441,18 @@
|
|||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
}
|
||||
.arco-select-option {
|
||||
margin: 0;
|
||||
.arco-select-option-checkbox {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
&:hover {
|
||||
.arco-checkbox-icon-hover::before {
|
||||
background-color: rgb(var(--primary-9)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ms-dropdown-divider {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
@ -442,6 +468,13 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.arco-select-dropdown-has-header {
|
||||
padding-top: 6px !important;
|
||||
}
|
||||
.arco-select-dropdown-footer {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.arco-dropdown-option-content {
|
||||
@apply flex items-center;
|
||||
|
||||
|
@ -742,3 +775,14 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/** Alter **/
|
||||
.arco-alert-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.arco-alert-icon {
|
||||
@apply flex;
|
||||
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
|
|
@ -66,16 +66,6 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
if (arr && arr.length > 0) {
|
||||
nextTick(() => {
|
||||
if (msCardListRef.value) {
|
||||
// 为了在列表数据滚动加载时,能够正确判断是否滚动到底部,因为此时没有触发滚动事件,而在加载前触发了滚动到底部的判断
|
||||
const listContent = msCardListRef.value;
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
isArrivedBottom.value = scrollHeight - clientHeight - scrollTop < props.shadowLimit;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
|
@ -219,7 +209,7 @@
|
|||
@apply overflow-hidden;
|
||||
.ms-container--shadow();
|
||||
.ms-card-list {
|
||||
@apply grid overflow-auto;
|
||||
@apply grid max-h-full overflow-auto;
|
||||
|
||||
.ms-scroll-bar();
|
||||
|
||||
|
|
|
@ -71,8 +71,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, Ref, nextTick, onMounted, computed, onBeforeUnmount } from 'vue';
|
||||
import { calculateMaxDepth } from '@/utils';
|
||||
import { ref, watch, Ref } from 'vue';
|
||||
import useSelect from '@/hooks/useSelect';
|
||||
|
||||
import type { CascaderOption } from '@arco-design/web-vue';
|
||||
import type { VirtualListProps } from '@arco-design/web-vue/es/_components/virtual-list-v2/interface';
|
||||
|
@ -102,11 +102,15 @@
|
|||
|
||||
const innerValue = ref<CascaderModelValue>([]);
|
||||
const innerLevel = ref(''); // 顶级选项,该级别为单选选项
|
||||
const maxTagCount = ref(1); // 最大显示 tag 数量
|
||||
const cascader: Ref = ref(null);
|
||||
const cascaderWidth = ref(0); // cascader 宽度
|
||||
const cascaderDeep = ref(1); // 默认层级只有一层
|
||||
const cascaderViewInner = ref<HTMLElement | null>(null); // 输入框内容容器 DOM
|
||||
|
||||
const { maxTagCount, getOptionComputedStyle, calculateMaxTag } = useSelect({
|
||||
selectRef: cascader,
|
||||
selectVal: innerValue,
|
||||
isCascade: true,
|
||||
options: props.options,
|
||||
panelWidth: props.panelWidth,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
|
@ -155,27 +159,6 @@
|
|||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
(arr) => {
|
||||
cascaderDeep.value = calculateMaxDepth(arr);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
const getOptionComputedStyle = computed(() => {
|
||||
// 减去 80px 是为了防止溢出,因为会出现单选框、右侧箭头
|
||||
return {
|
||||
width:
|
||||
cascaderDeep.value <= 2
|
||||
? `${cascaderWidth.value / cascaderDeep.value - 80 - cascaderDeep.value * 4}px`
|
||||
: `${props.panelWidth}px` || '150px',
|
||||
};
|
||||
});
|
||||
|
||||
interface CascaderValue {
|
||||
level: keyof typeof props.levelTop;
|
||||
value: string;
|
||||
|
@ -203,42 +186,12 @@
|
|||
innerLevel.value = '';
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
if (cascader.value && cascaderViewInner.value && Array.isArray(innerValue.value)) {
|
||||
if (maxTagCount.value !== 0 && innerValue.value.length > maxTagCount.value) return; // 已经超过最大数量的展示,不需要再计算
|
||||
let lastWidth = cascaderViewInner.value?.getBoundingClientRect().width || 0;
|
||||
const tags = cascaderViewInner.value.querySelectorAll('.arco-tag');
|
||||
let tagCount = 0;
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tagWidth = Number(getComputedStyle(tags[i]).width.replace('px', ''));
|
||||
if (lastWidth < tagWidth + 65) {
|
||||
// 65px 是“+N”的标签宽度+聚焦输入框的宽度
|
||||
lastWidth = 0; // 当剩余宽度已经放不下刚添加的标签,则剩余宽度置为 0,避免后面再进行计算
|
||||
break;
|
||||
} else {
|
||||
tagCount += 1;
|
||||
lastWidth = lastWidth - tagWidth - 5;
|
||||
}
|
||||
}
|
||||
maxTagCount.value = tagCount === 0 ? 1 : tagCount;
|
||||
}
|
||||
});
|
||||
calculateMaxTag();
|
||||
}
|
||||
|
||||
function clearValues() {
|
||||
innerLevel.value = '';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (cascader.value) {
|
||||
cascaderWidth.value = cascader.value.$el.nextElementSibling.getBoundingClientRect().width;
|
||||
cascaderViewInner.value = cascader.value.$el.nextElementSibling.querySelector('.arco-select-view-inner');
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
cascaderViewInner.value = null; // 释放 DOM 引用
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
@ -273,3 +226,4 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
@/hooks/useSelect
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="mt-[4px] w-full text-[12px] text-[var(--color-text-4)]">
|
||||
<div class="mt-[4px] flex w-full items-center text-[12px] text-[var(--color-text-4)]">
|
||||
{{ props.text }}
|
||||
<MsIcon
|
||||
v-if="props.showFillIcon"
|
||||
|
@ -7,6 +7,13 @@
|
|||
class="cursor-pointer text-[rgb(var(--primary-6))]"
|
||||
@click="fillHeapByDefault"
|
||||
></MsIcon>
|
||||
<MsIcon
|
||||
v-else-if="props.icon"
|
||||
:type="props.icon"
|
||||
class="cursor-pointer text-[rgb(var(--primary-6))]"
|
||||
@click="emit('iconClick')"
|
||||
></MsIcon>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -17,12 +24,14 @@
|
|||
defineProps<{
|
||||
text: string;
|
||||
showFillIcon?: boolean;
|
||||
icon?: string;
|
||||
iconText?: string;
|
||||
}>(),
|
||||
{
|
||||
showFillIcon: true,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits(['fill']);
|
||||
const emit = defineEmits(['fill', 'iconClick']);
|
||||
|
||||
function fillHeapByDefault() {
|
||||
emit('fill');
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { watch, ref, h, defineComponent, onBeforeMount } from 'vue';
|
||||
import { watch, ref, h, defineComponent, onBeforeMount, computed, Ref, Slot } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useSelect from '@/hooks/useSelect';
|
||||
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
|
@ -15,11 +16,14 @@ export type RemoteFieldsMap = {
|
|||
export interface MsSearchSelectProps {
|
||||
mode?: 'static' | 'remote'; // 静态模式,远程模式。默认为静态模式,需要传入 options 数据;远程模式需要传入请求函数
|
||||
modelValue: ModelType;
|
||||
allowSearch?: boolean;
|
||||
allowClear?: boolean;
|
||||
placeholder?: string;
|
||||
prefix?: string;
|
||||
searchKeys: string[]; // 需要搜索的 key 名,关键字会遍历这个 key 数组,然后取 item[key] 进行模糊匹配
|
||||
hasAllSelect?: boolean; // 是否有全选选项
|
||||
searchKeys?: string[]; // 需要搜索的 key 名,关键字会遍历这个 key 数组,然后取 item[key] 进行模糊匹配
|
||||
valueKey?: string; // 选项的 value 字段名,默认为 value
|
||||
options: SelectOptionData[];
|
||||
multiple?: boolean; // 是否多选
|
||||
remoteFieldsMap?: RemoteFieldsMap; // 远程模式下的结果 key 映射,例如 { value: 'id' },表示远程请求时,会将返回结果的 id 赋值到 value 字段
|
||||
remoteExtraParams?: Record<string, any>; // 远程模式下的额外参数
|
||||
remoteFunc?(params: Record<string, any>): Promise<any>; // 远程模式下的请求函数,返回一个 Promise
|
||||
|
@ -27,13 +31,20 @@ export interface MsSearchSelectProps {
|
|||
optionTooltipContent?: (item: SelectOptionData) => string; // 自定义 option 的 tooltip 内容,返回一个字符串,默认使用 item.label
|
||||
}
|
||||
|
||||
export interface MsSearchSelectSlots {
|
||||
prefix?: string;
|
||||
header?: (() => JSX.Element) | Slot<any>;
|
||||
default?: () => JSX.Element[];
|
||||
footer?: Slot<any>;
|
||||
}
|
||||
|
||||
export default defineComponent(
|
||||
(props: MsSearchSelectProps, { emit }) => {
|
||||
(props: MsSearchSelectProps & MsSearchSelectSlots, { emit, slots }) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const innerValue = ref(props.modelValue);
|
||||
const filterOptions = ref<SelectOptionData[]>([]);
|
||||
const remoteOriginOptions = ref<SelectOptionData[]>([...props.options]);
|
||||
const filterOptions = ref<SelectOptionData[]>([]); // 实际渲染的 options,会根据搜索关键字进行过滤
|
||||
const remoteOriginOptions = ref<SelectOptionData[]>([...props.options]); // 远程模式下的原始 options,接口返回的数据会存储在这里
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
|
@ -41,12 +52,14 @@ export default defineComponent(
|
|||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
(arr) => {
|
||||
filterOptions.value = [...arr];
|
||||
}
|
||||
);
|
||||
const selectRef = ref<Ref>();
|
||||
|
||||
const { maxTagCount, getOptionComputedStyle, calculateMaxTag } = useSelect({
|
||||
selectRef,
|
||||
selectVal: innerValue,
|
||||
isCascade: true,
|
||||
options: props.options,
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
|
@ -76,7 +89,11 @@ export default defineComponent(
|
|||
}
|
||||
if (val.trim() === '') {
|
||||
// 如果搜索关键字为空,则直接返回所有数据
|
||||
filterOptions.value = [...remoteOriginOptions.value];
|
||||
filterOptions.value = remoteOriginOptions.value.map((e) => ({
|
||||
...e,
|
||||
tooltipContent: typeof props.optionTooltipContent === 'function' ? props.optionTooltipContent(e) : e.label,
|
||||
}));
|
||||
calculateMaxTag();
|
||||
return;
|
||||
}
|
||||
const highlightedKeyword = `<span class="text-[rgb(var(--primary-4))]">${val}</span>`;
|
||||
|
@ -84,13 +101,15 @@ export default defineComponent(
|
|||
.map((e) => {
|
||||
const item = { ...e };
|
||||
let hasMatch = false;
|
||||
for (let i = 0; i < props.searchKeys.length; i++) {
|
||||
// 遍历传入的搜索字段
|
||||
const key = props.searchKeys[i];
|
||||
if (e[key].includes(val)) {
|
||||
// 是否匹配
|
||||
hasMatch = true;
|
||||
item[key] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
||||
if (props.searchKeys) {
|
||||
for (let i = 0; i < props.searchKeys.length; i++) {
|
||||
// 遍历传入的搜索字段
|
||||
const key = props.searchKeys[i];
|
||||
if (e[key].includes(val)) {
|
||||
// 是否匹配
|
||||
hasMatch = true;
|
||||
item[key] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasMatch) {
|
||||
|
@ -99,7 +118,9 @@ export default defineComponent(
|
|||
return null;
|
||||
})
|
||||
.filter((e) => e) as SelectOptionData[];
|
||||
calculateMaxTag();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
@ -111,31 +132,92 @@ export default defineComponent(
|
|||
? h('div', { innerHTML: props.optionLabelRender(item) })
|
||||
: item.label;
|
||||
|
||||
// 半选状态
|
||||
const indeterminate = computed(() => {
|
||||
if (props.multiple && Array.isArray(innerValue.value)) {
|
||||
return innerValue.value.length > 0 && innerValue.value.length < filterOptions.value.length;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const isSelectAll = computed({
|
||||
get: () => {
|
||||
if (props.multiple && Array.isArray(innerValue.value)) {
|
||||
return innerValue.value.length === filterOptions.value.length;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
set: (val) => val,
|
||||
});
|
||||
function handleSelectAllChange(val: boolean) {
|
||||
isSelectAll.value = val;
|
||||
if (val) {
|
||||
innerValue.value = filterOptions.value.map((item) => item[props.valueKey || 'value']);
|
||||
emit('update:modelValue', innerValue.value);
|
||||
} else {
|
||||
innerValue.value = [];
|
||||
emit('update:modelValue', []);
|
||||
}
|
||||
}
|
||||
|
||||
const selectSlots = () => {
|
||||
const _slots: MsSearchSelectSlots = {
|
||||
default: () =>
|
||||
filterOptions.value.map((item) => (
|
||||
<a-tooltip content={item.tooltipContent} mouse-enter-delay={500}>
|
||||
<a-option key={item.id} value={item[props.valueKey || 'value']}>
|
||||
<div class="one-line-text" style={getOptionComputedStyle.value}>
|
||||
{optionItemLabelRender(item)}
|
||||
</div>
|
||||
</a-option>
|
||||
</a-tooltip>
|
||||
)),
|
||||
};
|
||||
if (props.hasAllSelect) {
|
||||
_slots.header = () => (
|
||||
<div class="arco-select-option mb-[4px] h-[30px] rounded-[var(--border-radius-small)] !p-[3px_8px]">
|
||||
<a-checkbox
|
||||
model-value={isSelectAll.value}
|
||||
indeterminate={indeterminate.value}
|
||||
class="w-full"
|
||||
onChange={handleSelectAllChange}
|
||||
>
|
||||
{t('common.allSelect')}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (slots.header) {
|
||||
_slots.header = slots.header;
|
||||
}
|
||||
if (slots.footer) {
|
||||
_slots.footer = slots.footer;
|
||||
}
|
||||
|
||||
return _slots;
|
||||
};
|
||||
|
||||
onBeforeMount(() => {
|
||||
handleUserSearch('');
|
||||
});
|
||||
|
||||
return () => (
|
||||
<a-select
|
||||
ref={selectRef}
|
||||
default-value={innerValue}
|
||||
placeholder={t(props.placeholder || '')}
|
||||
allow-clear={props.allowClear}
|
||||
allow-search
|
||||
allow-search={props.allowSearch}
|
||||
filter-option={false}
|
||||
loading={loading.value}
|
||||
multiple={props.multiple}
|
||||
max-tag-count={maxTagCount.value}
|
||||
onUpdate:model-value={(value: ModelType) => emit('update:modelValue', value)}
|
||||
onInputValueChange={debounce(handleUserSearch, 300)}
|
||||
>
|
||||
{{
|
||||
prefix: () => t(props.prefix || ''),
|
||||
default: () =>
|
||||
filterOptions.value.map((item) => (
|
||||
<a-tooltip content={item.tooltipContent} mouse-enter-delay={500}>
|
||||
<a-option key={item.id} value={item.value}>
|
||||
{optionItemLabelRender(item)}
|
||||
</a-option>
|
||||
</a-tooltip>
|
||||
)),
|
||||
...selectSlots(),
|
||||
}}
|
||||
</a-select>
|
||||
);
|
||||
|
@ -145,16 +227,20 @@ export default defineComponent(
|
|||
props: [
|
||||
'mode',
|
||||
'modelValue',
|
||||
'allowSearch',
|
||||
'allowClear',
|
||||
'placeholder',
|
||||
'prefix',
|
||||
'searchKeys',
|
||||
'valueKey',
|
||||
'options',
|
||||
'optionLabelRender',
|
||||
'remoteFieldsMap',
|
||||
'remoteExtraParams',
|
||||
'remoteFunc',
|
||||
'optionTooltipContent',
|
||||
'prefix',
|
||||
'hasAllSelect',
|
||||
'multiple',
|
||||
],
|
||||
emits: ['update:modelValue'],
|
||||
}
|
|
@ -39,7 +39,7 @@
|
|||
}
|
||||
|
||||
padding: 0 4px;
|
||||
font-size: 1rem;
|
||||
font-size: 14px;
|
||||
border-radius: var(--border-radius-mini);
|
||||
line-height: 22px;
|
||||
}
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
<template>
|
||||
<div :class="`ms-button ms-button-${props.type} ms-button--${props.status}`" @click="clickHandler">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: 'text' | 'icon' | 'button';
|
||||
status?: 'primary' | 'danger' | 'secondary';
|
||||
}>(),
|
||||
{
|
||||
type: 'text',
|
||||
status: 'primary',
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
function clickHandler() {
|
||||
emit('click');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ms-button {
|
||||
@apply flex cursor-pointer items-center align-middle;
|
||||
|
||||
padding: 0 4px;
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius-mini);
|
||||
line-height: 22px;
|
||||
}
|
||||
.ms-button-text {
|
||||
@apply p-0;
|
||||
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
.ms-button-icon {
|
||||
padding: 4px;
|
||||
color: var(--color-text-4);
|
||||
&:hover {
|
||||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-9));
|
||||
.arco-icon {
|
||||
color: rgb(var(--primary-5));
|
||||
}
|
||||
}
|
||||
}
|
||||
.ms-button--secondary {
|
||||
color: var(--color-text-2);
|
||||
&:not(.ms-button-text):hover {
|
||||
background-color: var(--color-text-n8);
|
||||
}
|
||||
}
|
||||
.ms-button--primary {
|
||||
color: rgb(var(--primary-5));
|
||||
&:not(.ms-button-text):hover {
|
||||
background-color: rgb(var(--primary-9));
|
||||
}
|
||||
}
|
||||
.ms-button--danger {
|
||||
color: rgb(var(--danger-6));
|
||||
&:not(.ms-button-text):hover {
|
||||
color: rgb(var(--danger-6));
|
||||
background-color: rgb(var(--danger-1));
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -5,6 +5,7 @@
|
|||
'ms-card',
|
||||
'relative',
|
||||
'h-full',
|
||||
props.isFullscreen ? 'ms-card--no-radius' : '',
|
||||
props.autoHeight ? '' : 'min-h-[500px]',
|
||||
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
|
||||
]"
|
||||
|
@ -15,17 +16,8 @@
|
|||
</div>
|
||||
<a-divider v-if="!props.simple" class="mb-[16px]" />
|
||||
<div class="ms-card-container">
|
||||
<a-scrollbar
|
||||
class="pr-[5px]"
|
||||
:style="{
|
||||
overflow: 'auto',
|
||||
width: props.otherWidth
|
||||
? `calc(100vw - ${menuWidth}px - ${props.otherWidth}px)`
|
||||
: `calc(100vw - ${menuWidth}px - 58px)`,
|
||||
height: props.autoHeight ? 'auto' : `calc(100vh - ${cardOverHeight}px)`,
|
||||
}"
|
||||
>
|
||||
<div :style="{ minWidth: `${props.minWidth || 1000}px` }">
|
||||
<a-scrollbar class="pr-[5px]" :style="getComputedContentStyle">
|
||||
<div class="relative h-full w-full" :style="{ minWidth: `${props.minWidth || 1000}px` }">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</a-scrollbar>
|
||||
|
@ -72,10 +64,11 @@
|
|||
specialHeight: number; // 特殊高度,例如某些页面有面包屑
|
||||
hideBack: boolean; // 隐藏返回按钮
|
||||
autoHeight: boolean; // 内容区域高度是否自适应
|
||||
otherWidth?: number; // 该宽度为卡片外部同级容器的宽度
|
||||
minWidth?: number; // 卡片最小宽度
|
||||
otherWidth: number; // 该宽度为卡片外部同级容器的宽度
|
||||
minWidth: number; // 卡片最小宽度
|
||||
hasBreadcrumb: boolean; // 是否有面包屑,如果有面包屑,高度需要减去面包屑的高度
|
||||
noContentPadding: boolean; // 内容区域是否有padding
|
||||
isFullscreen?: boolean; // 是否全屏
|
||||
handleBack: () => void; // 自定义返回按钮触发事件
|
||||
}>
|
||||
>(),
|
||||
|
@ -117,6 +110,23 @@
|
|||
return 246 + _specialHeight;
|
||||
});
|
||||
|
||||
const getComputedContentStyle = computed(() => {
|
||||
if (props.isFullscreen) {
|
||||
return {
|
||||
overflow: 'auto',
|
||||
width: 'calc(100vw - 58px)',
|
||||
height: 'auto',
|
||||
};
|
||||
}
|
||||
return {
|
||||
overflow: 'auto',
|
||||
width: props.otherWidth
|
||||
? `calc(100vw - ${menuWidth.value}px - ${props.otherWidth}px)`
|
||||
: `calc(100vw - ${menuWidth.value}px - 58px)`,
|
||||
height: props.autoHeight ? 'auto' : `calc(100vh - ${cardOverHeight.value}px)`,
|
||||
};
|
||||
});
|
||||
|
||||
function back() {
|
||||
if (typeof props.handleBack === 'function') {
|
||||
props.handleBack();
|
||||
|
@ -128,7 +138,7 @@
|
|||
|
||||
<style lang="less" scoped>
|
||||
.ms-card {
|
||||
@apply overflow-hidden bg-white;
|
||||
@apply relative overflow-hidden bg-white;
|
||||
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: 0 0 10px rgb(120 56 135 / 5%);
|
||||
|
@ -158,4 +168,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.ms-card--no-radius {
|
||||
border-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -44,15 +44,20 @@
|
|||
</a-scrollbar>
|
||||
<template #footer>
|
||||
<slot name="footer">
|
||||
<a-button :disabled="props.okLoading" @click="handleCancel">
|
||||
{{ t(props.cancelText || 'ms.drawer.cancel') }}
|
||||
</a-button>
|
||||
<a-button v-if="showContinue" type="secondary" :loading="props.okLoading" @click="handleContinue">
|
||||
{{ t(props.saveContinueText || 'ms.drawer.saveContinue') }}
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="props.okLoading" @click="handleOk">
|
||||
{{ t(props.okText || 'ms.drawer.ok') }}
|
||||
</a-button>
|
||||
<div class="flex items-center justify-between">
|
||||
<slot name="footerLeft"></slot>
|
||||
<div class="ml-auto flex gap-[12px]">
|
||||
<a-button :disabled="props.okLoading" @click="handleCancel">
|
||||
{{ t(props.cancelText || 'ms.drawer.cancel') }}
|
||||
</a-button>
|
||||
<a-button v-if="showContinue" type="secondary" :loading="props.okLoading" @click="handleContinue">
|
||||
{{ t(props.saveContinueText || 'ms.drawer.saveContinue') }}
|
||||
</a-button>
|
||||
<a-button type="primary" :loading="props.okLoading" @click="handleOk">
|
||||
{{ t(props.okText || 'ms.drawer.ok') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
|
|
@ -154,14 +154,6 @@
|
|||
initScrollListener();
|
||||
});
|
||||
}
|
||||
if (props.data.length > 0) {
|
||||
nextTick(() => {
|
||||
// 为了在列表数据滚动加载时,能够正确判断是否滚动到底部,因为此时没有触发滚动事件,而在加载前触发了滚动到底部的判断
|
||||
const listContent = listRef.value?.$el.querySelector('.arco-list-content');
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
isArrivedBottom.value = scrollHeight - clientHeight - scrollTop < props.itemHeight;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
|
|
|
@ -84,12 +84,6 @@
|
|||
listContent.addEventListener('scroll', listenScroll);
|
||||
});
|
||||
}
|
||||
nextTick(() => {
|
||||
// 为了在列表数据滚动加载时,能够正确判断是否滚动到底部,因为此时没有触发滚动事件,而在加载前触发了滚动到底部的判断
|
||||
const listContent = listRef.value?.$el.querySelector('.arco-list-content');
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
isArrivedBottom.value = scrollHeight - clientHeight - scrollTop < 20;
|
||||
});
|
||||
});
|
||||
|
||||
function handleReachBottom() {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="navbar">
|
||||
<div class="left-side">
|
||||
<a-space>
|
||||
<div class="flex max-w-[145px] items-center overflow-hidden">
|
||||
<div class="one-line-text flex max-w-[145px] items-center">
|
||||
<img :src="props.logo" class="mr-[4px] h-[32px] w-[32px]" />
|
||||
{{ props.name }}
|
||||
</div>
|
||||
|
|
|
@ -225,6 +225,20 @@ export const pathMap: PathMapItem[] = [
|
|||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
{
|
||||
key: 'PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT', // 项目管理-消息管理
|
||||
locale: 'menu.projectManagement.messageManagement',
|
||||
route: RouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
{
|
||||
key: 'PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_EDIT', // 项目管理-消息管理-编辑
|
||||
locale: 'menu.projectManagement.messageManagementEdit',
|
||||
route: RouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_EDIT,
|
||||
permission: [],
|
||||
level: MENU_LEVEL[2],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -18,6 +18,8 @@ export enum PerformanceTestRouteEnum {
|
|||
export enum ProjectManagementRouteEnum {
|
||||
PROJECT_MANAGEMENT = 'projectManagement',
|
||||
PROJECT_MANAGEMENT_FILE_MANAGEMENT = 'projectManagementFileManageMent',
|
||||
PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT = 'projectManagementMessageManagement',
|
||||
PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_EDIT = 'projectManagementMessageManagementEdit',
|
||||
PROJECT_MANAGEMENT_LOG = 'projectManagementLog',
|
||||
PROJECT_MANAGEMENT_PERMISSION = 'projectManagementPermission',
|
||||
PROJECT_MANAGEMENT_PERMISSION_BASIC_INFO = 'projectManagementPermissionBasicInfo',
|
||||
|
|
|
@ -31,6 +31,13 @@ export default function useContainerShadow(options: ContainerShadowOptions) {
|
|||
}
|
||||
}
|
||||
|
||||
function calculateArrivedPosition(listContent: HTMLElement) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
const scrollBottom = scrollHeight - clientHeight - scrollTop;
|
||||
isArrivedTop.value = scrollTop < options.overHeight;
|
||||
isArrivedBottom.value = scrollBottom < options.overHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听列表内容区域滚动,以切换顶部底部阴影
|
||||
* @param event 滚动事件
|
||||
|
@ -38,16 +45,14 @@ export default function useContainerShadow(options: ContainerShadowOptions) {
|
|||
function listenScroll(event: Event) {
|
||||
if (event.target) {
|
||||
const listContent = event.target as HTMLElement;
|
||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
||||
const scrollBottom = scrollHeight - clientHeight - scrollTop;
|
||||
isArrivedTop.value = scrollTop < options.overHeight;
|
||||
isArrivedBottom.value = scrollBottom < options.overHeight;
|
||||
calculateArrivedPosition(listContent);
|
||||
}
|
||||
}
|
||||
|
||||
function initScrollListener() {
|
||||
if (!isInitListener.value && containerRef.value) {
|
||||
containerRef.value.addEventListener('scroll', listenScroll);
|
||||
calculateArrivedPosition(containerRef.value); // 初始化计算一次,因为初始化的时候内容可能超出可视区域了
|
||||
isInitListener.value = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { Ref, computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { calculateMaxDepth } from '@/utils';
|
||||
|
||||
import type { CascaderOption, SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
export interface UseSelectOption {
|
||||
selectRef: Ref; // 选择器 ref 对象
|
||||
selectVal: Ref; // 选择器的 v-model
|
||||
isCascade?: boolean; // 是否级联选择器
|
||||
panelWidth?: number; // 级联选择器的下拉面板宽度
|
||||
options?: CascaderOption[] | SelectOptionData[]; // 级联选择器的选项
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Select 组件的单行展示的最大标签数量
|
||||
* @param selectRef 选择器 ref 对象
|
||||
* @param selectVal 选择器的 v-model
|
||||
*/
|
||||
export default function useSelect(config: UseSelectOption) {
|
||||
const maxTagCount = ref(1);
|
||||
const selectWidth = ref(0);
|
||||
const selectViewInner = ref<HTMLElement | null>(null); // 输入框内容容器 DOM
|
||||
const cascadeDeep = ref(0); // 级联选择器的深度
|
||||
|
||||
/**
|
||||
* 计算最大标签数量
|
||||
*/
|
||||
function calculateMaxTag() {
|
||||
nextTick(() => {
|
||||
if (config.selectRef.value && selectViewInner.value && Array.isArray(config.selectVal.value)) {
|
||||
if (maxTagCount.value !== 0 && config.selectVal.value.length > maxTagCount.value) return; // 已经超过最大数量的展示,不需要再计算
|
||||
let lastWidth = selectViewInner.value?.getBoundingClientRect().width || 0;
|
||||
const tags = selectViewInner.value.querySelectorAll('.arco-tag');
|
||||
let tagCount = 0;
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tagWidth = Number(getComputedStyle(tags[i]).width.replace('px', ''));
|
||||
if (lastWidth < tagWidth + 65) {
|
||||
// 65px 是“+N”的标签宽度+聚焦输入框的宽度
|
||||
lastWidth = 0; // 当剩余宽度已经放不下刚添加的标签,则剩余宽度置为 0,避免后面再进行计算
|
||||
break;
|
||||
} else {
|
||||
tagCount += 1;
|
||||
lastWidth = lastWidth - tagWidth - 5;
|
||||
}
|
||||
}
|
||||
maxTagCount.value = tagCount === 0 ? 1 : tagCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getOptionComputedStyle = computed(() => {
|
||||
if (config.isCascade) {
|
||||
// 减去 80px 是为了防止溢出,因为会出现单选框、右侧箭头
|
||||
return {
|
||||
width:
|
||||
cascadeDeep.value <= 2
|
||||
? `${selectWidth.value / cascadeDeep.value - 80 - cascadeDeep.value * 4}px`
|
||||
: `${config.panelWidth}px` || '150px',
|
||||
};
|
||||
}
|
||||
// 减去 60px 是为了防止溢出,因为有复选框、边距等
|
||||
return {
|
||||
width: `${selectWidth.value - 60}px`,
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
() => config.options,
|
||||
(arr) => {
|
||||
if (config.isCascade && arr && arr.length > 0) {
|
||||
// 级联选择器的选项发生变化时,重新计算最大深度
|
||||
cascadeDeep.value = calculateMaxDepth(arr);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (config.selectRef.value) {
|
||||
selectWidth.value = config.selectRef.value.$el.nextElementSibling.getBoundingClientRect().width;
|
||||
selectViewInner.value = config.selectRef.value.$el.nextElementSibling.querySelector('.arco-select-view-inner');
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
selectViewInner.value = null; // 释放 DOM 引用
|
||||
});
|
||||
|
||||
return {
|
||||
maxTagCount,
|
||||
getOptionComputedStyle, // 获取选择器选项的样式
|
||||
calculateMaxTag, // 在需要的时机调用此函数以计算最大标签数量,一般在 select 的 change 事件中调用
|
||||
};
|
||||
}
|
|
@ -55,4 +55,7 @@ export default {
|
|||
'common.internal': 'Internal',
|
||||
'common.custom': 'Custom',
|
||||
'common.preview': 'Preview',
|
||||
'common.fullScreen': 'Full Screen',
|
||||
'common.offFullScreen': 'Exit',
|
||||
'common.allSelect': 'Select All',
|
||||
};
|
||||
|
|
|
@ -28,6 +28,8 @@ export default {
|
|||
'menu.performanceTest': 'Performance Test',
|
||||
'menu.projectManagement': 'Project',
|
||||
'menu.projectManagement.fileManagement': 'File Management',
|
||||
'menu.projectManagement.messageManagement': 'Message Management',
|
||||
'menu.projectManagement.messageManagementEdit': 'Update Template',
|
||||
'menu.featureTest.featureCase': 'Feature Case',
|
||||
'meun.workstation': 'Workstation',
|
||||
'menu.loadTest': 'Performance Test',
|
||||
|
|
|
@ -57,4 +57,7 @@ export default {
|
|||
'common.internal': '内部',
|
||||
'common.custom': '自定义',
|
||||
'common.preview': '预览',
|
||||
'common.fullScreen': '全屏',
|
||||
'common.offFullScreen': '退出全屏',
|
||||
'common.allSelect': '全选',
|
||||
};
|
||||
|
|
|
@ -31,6 +31,8 @@ export default {
|
|||
'menu.projectManagement': '项目管理',
|
||||
'menu.projectManagement.log': '日志',
|
||||
'menu.projectManagement.fileManagement': '文件管理',
|
||||
'menu.projectManagement.messageManagement': '消息管理',
|
||||
'menu.projectManagement.messageManagementEdit': '更新模板',
|
||||
'menu.featureTest.featureCase': '功能用例',
|
||||
'menu.projectManagement.projectPermission': '项目与权限',
|
||||
'menu.settings': '系统设置',
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// 支持添加机器人的平台类型
|
||||
export type ProjectRobotPlatformCanEdit = 'DING_TALK' | 'LARK' | 'WE_COM' | 'CUSTOM';
|
||||
|
||||
// 机器人全平台类型
|
||||
export type ProjectRobotPlatform = ProjectRobotPlatformCanEdit | 'IN_SITE' | 'MAIL';
|
||||
|
||||
// 钉钉机器人类型
|
||||
export type ProjectRobotDingTalkType = 'CUSTOM' | 'ENTERPRISE';
|
||||
|
||||
export interface RobotCommon {
|
||||
name: string;
|
||||
platform: ProjectRobotPlatformCanEdit;
|
||||
webhook: string;
|
||||
enable: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface RobotAddParams extends RobotCommon {
|
||||
projectId: string;
|
||||
type?: ProjectRobotDingTalkType;
|
||||
appKey?: string;
|
||||
appSecret?: string;
|
||||
}
|
||||
|
||||
export interface RobotEditParams extends RobotAddParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface RobotItem extends Omit<RobotEditParams, 'platform'> {
|
||||
platform: ProjectRobotPlatform;
|
||||
createUser: string;
|
||||
createTime: number;
|
||||
updateUser?: string;
|
||||
updateTime?: number;
|
||||
}
|
|
@ -90,6 +90,38 @@ const ProjectManagement: AppRouteRecordRaw = {
|
|||
isTopMenu: true,
|
||||
},
|
||||
},
|
||||
// 消息管理
|
||||
{
|
||||
path: 'messageManagement',
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT,
|
||||
component: () => import('@/views/project-management/messageManagement/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.projectManagement.messageManagement',
|
||||
roles: ['*'],
|
||||
isTopMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'messageManagementEdit',
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_EDIT,
|
||||
component: () => import('@/views/project-management/messageManagement/edit.vue'),
|
||||
meta: {
|
||||
locale: 'menu.projectManagement.messageManagementEdit',
|
||||
roles: ['*'],
|
||||
breadcrumbs: [
|
||||
{
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT,
|
||||
locale: 'menu.projectManagement.messageManagement',
|
||||
},
|
||||
{
|
||||
name: ProjectManagementRouteEnum.PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_EDIT,
|
||||
locale: 'menu.projectManagement.messageManagementEdit',
|
||||
editTag: 'id',
|
||||
editLocale: 'menu.projectManagement.messageManagementEdit',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// 项目日志
|
||||
{
|
||||
path: 'log',
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<a-input
|
||||
v-else
|
||||
v-model:model-value="form.field"
|
||||
:max-length="props.fieldConfig?.maxLength"
|
||||
:max-length="props.fieldConfig?.maxLength || 50"
|
||||
:placeholder="props.fieldConfig?.placeholder || t('project.fileManagement.namePlaceholder')"
|
||||
class="w-[245px]"
|
||||
@press-enter="beforeConfirm(undefined)"
|
||||
|
@ -48,7 +48,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeMount, ref, watch } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
|
|
|
@ -71,6 +71,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 项目管理-文件管理
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<template> ProjectManagement is waiting for development </template>
|
||||
|
||||
<script setup></script>
|
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<MsCard ref="fullRef" :special-height="132" :is-fullscreen="isFullscreen" simple>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-medium text-[var(--color-text-000)]">{{ t('project.messageManagement.config') }}</div>
|
||||
<div>
|
||||
<MsSelect
|
||||
v-model:model-value="robotFilters"
|
||||
:options="robotOptions"
|
||||
:allow-search="false"
|
||||
class="mr-[8px] w-[240px]"
|
||||
:prefix="t('project.messageManagement.robot')"
|
||||
value-key="id"
|
||||
:multiple="true"
|
||||
:has-all-select="true"
|
||||
>
|
||||
<template #footer>
|
||||
<div class="mb-[6px] mt-[4px] p-[3px_8px]">
|
||||
<MsButton type="text" @click="emit('createRobot')">
|
||||
<MsIcon type="icon-icon_add_outlined" class="mr-[8px] text-[rgb(var(--primary-6))]" size="14" />
|
||||
{{ t('project.messageManagement.createBot') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
</MsSelect>
|
||||
<a-button type="outline" class="arco-btn-outline--secondary px-[5px]" @click="toggle">
|
||||
<template #icon>
|
||||
<MsIcon
|
||||
:type="isFullscreen ? 'icon-icon_off_screen' : 'icon-icon_full_screen_one'"
|
||||
class="text-[var(--color-text-4)]"
|
||||
size="14"
|
||||
/>
|
||||
</template>
|
||||
{{ t(isFullscreen ? 'common.offFullScreen' : 'common.fullScreen') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</MsCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsSelect from '@/components/business/ms-select';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
|
||||
const emit = defineEmits(['createRobot']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const robotFilters = ref([]);
|
||||
const robotOptions = ref<SelectOptionData[]>([
|
||||
{
|
||||
label: '机器人1',
|
||||
id: 'robot1',
|
||||
},
|
||||
{
|
||||
label: '机器人2',
|
||||
id: 'robot2',
|
||||
},
|
||||
]);
|
||||
const fullRef = ref<HTMLElement | null>();
|
||||
|
||||
const { isFullscreen, toggle } = useFullscreen(fullRef);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,547 @@
|
|||
<template>
|
||||
<div>
|
||||
<MsCard :min-width="1060" :special-height="132" simple>
|
||||
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" closable @close="addVisited">
|
||||
{{ t('project.messageManagement.botListTips') }}
|
||||
<template #close-element>
|
||||
<span class="text-[14px]">{{ t('project.messageManagement.notRemind') }}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
<a-button type="primary" class="mb-[16px]" @click="handleCreateClick">
|
||||
{{ t('project.messageManagement.createBot') }}
|
||||
</a-button>
|
||||
<div
|
||||
ref="robotListContainerRef"
|
||||
:class="['robot-list-container', containerStatusClass]"
|
||||
:style="{ height: `calc(100% - ${getIsVisited() ? 48 : 104}px)` }"
|
||||
>
|
||||
<div ref="robotListRef" class="robot-list">
|
||||
<div v-for="robot of botList" :key="robot.id" class="robot-card">
|
||||
<div class="flex">
|
||||
<MsIcon
|
||||
:type="IconMap[robot.platform]"
|
||||
class="mr-[8px] h-[40px] w-[40px] bg-[var(--color-text-n9)] p-[8px] text-[rgb(var(--primary-5))]"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ robot.name }}</div>
|
||||
<div v-if="['IN_SITE', 'MAIL'].includes(robot.platform)" class="text-[12px] text-[var(--color-text-4)]">
|
||||
{{ robot.description }}
|
||||
</div>
|
||||
<div v-else class="flex items-center text-[12px] text-[var(--color-text-4)]">
|
||||
<div class="mr-[16px]">
|
||||
<a-tooltip position="tl" mini :content="robot.createUser">{{ robot.createUser }}</a-tooltip>
|
||||
{{
|
||||
`${t('project.messageManagement.createAt')} ${dayjs(robot.createTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}`
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
{{
|
||||
`${robot.updateUser} ${t('project.messageManagement.updateAt')} ${dayjs(robot.updateTime).format(
|
||||
'YYYY-MM-DD HH:mm:ss'
|
||||
)}`
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between leading-[24px]">
|
||||
<div v-if="!['IN_SITE', 'MAIL'].includes(robot.platform)">
|
||||
<a-button
|
||||
type="outline"
|
||||
size="mini"
|
||||
class="arco-btn-outline--secondary mr-[8px]"
|
||||
@click="editRobot(robot)"
|
||||
>
|
||||
{{ t('common.edit') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" class="arco-btn-outline--secondary" @click="deleteRobot(robot)">{{
|
||||
t('common.delete')
|
||||
}}</a-button>
|
||||
</div>
|
||||
<a-switch
|
||||
v-model:model-value="robot.enable"
|
||||
size="small"
|
||||
class="ml-auto"
|
||||
@change="handleEnableIntercept(robot)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MsCard>
|
||||
<MsDrawer
|
||||
v-model:visible="showDetailDrawer"
|
||||
:width="960"
|
||||
:title="t('project.messageManagement.createBot')"
|
||||
:ok-loading="drawerLoading"
|
||||
:show-continue="!isEdit"
|
||||
:ok-text="isEdit ? t('common.update') : t('common.create')"
|
||||
@confirm="handleDrawerConfirm"
|
||||
@continue="handleDrawerConfirm(true)"
|
||||
@cancel="handleDrawerCancel"
|
||||
>
|
||||
<a-form ref="robotFormRef" :model="robotForm" layout="vertical">
|
||||
<a-form-item :label="t('project.messageManagement.choosePlatform')" field="platform">
|
||||
<div class="grid w-full grid-cols-4 gap-[16px]">
|
||||
<div
|
||||
v-for="platform of editPlatformList"
|
||||
:key="platform.key"
|
||||
:class="['platform-card', robotForm.platform === platform.key ? 'platform-card--active' : '']"
|
||||
@click="robotForm.platform = platform.key"
|
||||
>
|
||||
<MsIcon
|
||||
:type="IconMap[platform.key]"
|
||||
class="h-[32px] w-[32px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[4px]"
|
||||
/>
|
||||
{{ platform.name }}
|
||||
</div>
|
||||
<div
|
||||
:class="['platform-card-custom', robotForm.platform === 'CUSTOM' ? 'platform-card--active' : '']"
|
||||
@click="robotForm.platform = 'CUSTOM'"
|
||||
>
|
||||
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[rgb(var(--primary-6))]" />
|
||||
<div class="ml-[8px] text-[rgb(var(--primary-6))]">{{ t('project.messageManagement.CUSTOM') }}</div>
|
||||
<div class="text-[var(--color-text-n4)]">{{ t('project.messageManagement.business') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label="t('project.messageManagement.name')"
|
||||
field="name"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('project.messageManagement.nameRequired') }]"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="robotForm.name"
|
||||
:max-length="250"
|
||||
:placeholder="t('project.messageManagement.namePlaceholder')"
|
||||
show-word-limit
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
v-if="robotForm.platform === 'DING_TALK'"
|
||||
:label="t('project.messageManagement.dingTalkType')"
|
||||
field="type"
|
||||
>
|
||||
<a-radio-group v-model:model-value="robotForm.type" type="button">
|
||||
<a-radio value="CUSTOM">
|
||||
{{ t('project.messageManagement.CUSTOM') }}
|
||||
</a-radio>
|
||||
<a-radio value="ENTERPRISE">
|
||||
{{ t('project.messageManagement.ENTERPRISE') }}
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
<template v-if="robotForm.type === 'CUSTOM'">
|
||||
<MsFormItemSub :text="t('project.messageManagement.dingTalkCustomTip')" :show-fill-icon="false">
|
||||
<MsButton
|
||||
type="text"
|
||||
class="ml-[8px] !text-[12px]"
|
||||
@click="openExternalLink('https://open.dingtalk.com/document/orgapp/custom-robot-access')"
|
||||
>
|
||||
<MsIcon type="icon-icon_share" size="12" class="mr-[4px]" />
|
||||
{{ t('project.messageManagement.noticeDetail') }}
|
||||
</MsButton>
|
||||
</MsFormItemSub>
|
||||
<a-alert :title="t('project.messageManagement.dingTalkCustomTitle')">
|
||||
<div class="text-[var(--color-text-2)]">{{ t('project.messageManagement.dingTalkCustomContent1') }}</div>
|
||||
<div class="text-[var(--color-text-2)]">
|
||||
{{ t('project.messageManagement.dingTalkCustomContent2', { at: '@' }) }}
|
||||
</div>
|
||||
<div class="text-[var(--color-text-2)]">{{ t('project.messageManagement.dingTalkCustomContent3') }}</div>
|
||||
</a-alert>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MsFormItemSub :text="t('project.messageManagement.dingTalkEnterpriseTip')" :show-fill-icon="false">
|
||||
<MsButton
|
||||
type="text"
|
||||
class="ml-[8px] !text-[12px]"
|
||||
@click="
|
||||
openExternalLink(
|
||||
'https://open.dingtalk.com/document/orgapp/the-creation-and-installation-of-the-application-robot-in-the'
|
||||
)
|
||||
"
|
||||
>
|
||||
<MsIcon type="icon-icon_share" size="12" class="mr-[4px]" />
|
||||
{{ t('project.messageManagement.helpDoc') }}
|
||||
</MsButton>
|
||||
</MsFormItemSub>
|
||||
<a-alert :title="t('project.messageManagement.dingTalkEnterpriseTitle')">
|
||||
<div class="text-[var(--color-text-2)]">
|
||||
{{ t('project.messageManagement.dingTalkEnterpriseContent1', { at: '@' }) }}
|
||||
</div>
|
||||
<div class="text-[var(--color-text-2)]">
|
||||
{{ t('project.messageManagement.dingTalkEnterpriseContent2') }}
|
||||
</div>
|
||||
</a-alert>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<template v-if="robotForm.platform === 'DING_TALK' && robotForm.type === 'ENTERPRISE'">
|
||||
<a-form-item
|
||||
:label="t('project.messageManagement.appKey')"
|
||||
field="appKey"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('project.messageManagement.appKeyRequired') }]"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="robotForm.appKey"
|
||||
:placeholder="t('project.messageManagement.appKeyPlaceholder')"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
:label="t('project.messageManagement.appSecret')"
|
||||
field="appSecret"
|
||||
asterisk-position="end"
|
||||
:rules="[{ required: true, message: t('project.messageManagement.appSecretRequired') }]"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="robotForm.appSecret"
|
||||
:placeholder="t('project.messageManagement.appSecretPlaceholder')"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item
|
||||
:label="t('project.messageManagement.webhook')"
|
||||
field="webhook"
|
||||
asterisk-position="end"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: t('project.messageManagement.webhookRequired'),
|
||||
},
|
||||
]"
|
||||
required
|
||||
>
|
||||
<a-input
|
||||
v-model:model-value="robotForm.webhook"
|
||||
:placeholder="
|
||||
t(
|
||||
robotForm.platform === 'CUSTOM'
|
||||
? 'project.messageManagement.webhookCustomPlaceholder'
|
||||
: 'project.messageManagement.webhookPlaceholder',
|
||||
{
|
||||
type: t(`project.messageManagement.${robotForm.platform}`),
|
||||
}
|
||||
)
|
||||
"
|
||||
allow-clear
|
||||
></a-input>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footerLeft>
|
||||
<a-switch v-model:model-value="robotForm.enable" size="small" class="mr-[4px]"></a-switch>
|
||||
{{ t('project.messageManagement.status') }}
|
||||
<a-tooltip position="tl" mini>
|
||||
<template #content>
|
||||
<div>{{ t('project.messageManagement.statusTipOn') }}</div>
|
||||
<div>{{ t('project.messageManagement.statusTipOff') }}</div>
|
||||
</template>
|
||||
<icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</MsDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { FormInstance, Message, ValidatedError } from '@arco-design/web-vue';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import useVisit from '@/hooks/useVisit';
|
||||
import {
|
||||
addRobot,
|
||||
updateRobot,
|
||||
getRobotList,
|
||||
getRobotDetail,
|
||||
toggleRobot,
|
||||
} from '@/api/modules/project-management/messageManagement';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { characterLimit } from '@/utils';
|
||||
import useContainerShadow from '@/hooks/useContainerShadow';
|
||||
|
||||
import type {
|
||||
ProjectRobotPlatform,
|
||||
RobotItem,
|
||||
RobotAddParams,
|
||||
RobotEditParams,
|
||||
ProjectRobotPlatformCanEdit,
|
||||
} from '@/models/projectManagement/message';
|
||||
|
||||
const props = defineProps<{
|
||||
activeTab: string;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const appStore = useAppStore();
|
||||
const { openModal } = useModal();
|
||||
|
||||
const botList = ref<RobotItem[]>([]);
|
||||
|
||||
const IconMap: Record<ProjectRobotPlatform, string> = {
|
||||
IN_SITE: 'icon-icon_bot1',
|
||||
MAIL: 'icon-icon_mail',
|
||||
LARK: 'icon-logo_lark',
|
||||
DING_TALK: 'icon-logo_dingtalk',
|
||||
WE_COM: 'icon-logo_wechat-work',
|
||||
CUSTOM: 'icon-icon_bot1',
|
||||
};
|
||||
|
||||
const robotListContainerRef = ref<HTMLDivElement | null>(null);
|
||||
const robotListRef = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const { containerStatusClass, setContainer, initScrollListener } = useContainerShadow({
|
||||
overHeight: 20,
|
||||
containerClassName: 'robot-list-container',
|
||||
});
|
||||
|
||||
async function initRobotList() {
|
||||
const res = await getRobotList(appStore.currentProjectId);
|
||||
botList.value = res as RobotItem[];
|
||||
nextTick(() => {
|
||||
if (robotListRef.value) {
|
||||
setContainer(robotListRef.value);
|
||||
initScrollListener();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.activeTab,
|
||||
(value) => {
|
||||
if (value === 'botList') {
|
||||
initRobotList();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const visitedKey = 'messageManagementRobotListTip';
|
||||
const { addVisited, getIsVisited } = useVisit(visitedKey);
|
||||
|
||||
const showDetailDrawer = ref(false);
|
||||
const drawerLoading = ref(false);
|
||||
const defaultRobot = {
|
||||
id: '',
|
||||
projectId: appStore.currentProjectId,
|
||||
name: '',
|
||||
platform: 'WE_COM',
|
||||
enable: false,
|
||||
webhook: '',
|
||||
type: 'CUSTOM',
|
||||
appKey: '',
|
||||
appSecret: '',
|
||||
} as RobotAddParams | RobotEditParams | RobotItem;
|
||||
const robotForm = ref({ ...defaultRobot });
|
||||
const robotFormRef = ref<FormInstance | null>(null);
|
||||
const isEdit = ref(false);
|
||||
const editPlatformList = ref<{ name: string; key: ProjectRobotPlatformCanEdit }[]>([
|
||||
{
|
||||
name: t('project.messageManagement.WE_COM'),
|
||||
key: 'WE_COM',
|
||||
},
|
||||
{
|
||||
name: t('project.messageManagement.DING_TALK'),
|
||||
key: 'DING_TALK',
|
||||
},
|
||||
{
|
||||
name: t('project.messageManagement.LARK'),
|
||||
key: 'LARK',
|
||||
},
|
||||
]);
|
||||
|
||||
function handleCreateClick() {
|
||||
isEdit.value = false;
|
||||
showDetailDrawer.value = true;
|
||||
robotForm.value = { ...defaultRobot };
|
||||
}
|
||||
|
||||
function editRobot(robot: RobotItem) {
|
||||
isEdit.value = true;
|
||||
robotForm.value = { ...robot };
|
||||
showDetailDrawer.value = true;
|
||||
}
|
||||
|
||||
function openExternalLink(url: string) {
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用机器人
|
||||
* @param robot 机器人信息
|
||||
*/
|
||||
function handleEnableIntercept(robot: RobotItem) {
|
||||
robot.enable = !robot.enable;
|
||||
openModal({
|
||||
type: robot.enable ? 'warning' : 'info',
|
||||
title: t(robot.enable ? 'project.messageManagement.disableTitle' : 'project.messageManagement.enableTitle', {
|
||||
name: characterLimit(robot.name),
|
||||
}),
|
||||
content: t(robot.enable ? 'project.messageManagement.disableContent' : 'project.messageManagement.enableContent'),
|
||||
okText: t(robot.enable ? 'project.messageManagement.disableConfirm' : 'project.messageManagement.enableConfirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
maskClosable: false,
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
Message.success(
|
||||
t(robot.enable ? 'project.messageManagement.disableSuccess' : 'project.messageManagement.enableSuccess')
|
||||
);
|
||||
initRobotList();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除机器人
|
||||
* @param robot 机器人信息
|
||||
*/
|
||||
function deleteRobot(robot: RobotItem) {
|
||||
openModal({
|
||||
type: 'error',
|
||||
title: t('project.messageManagement.deleteTitle', { name: robot.name }),
|
||||
content: t('project.messageManagement.deleteContent'),
|
||||
okText: t('common.confirmDelete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: {
|
||||
status: 'danger',
|
||||
},
|
||||
maskClosable: false,
|
||||
onBeforeOk: async () => {
|
||||
try {
|
||||
Message.success(t('common.deleteSuccess'));
|
||||
initRobotList();
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
hideCancel: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存机器人
|
||||
* @param isContinue 是否继续添加
|
||||
*/
|
||||
async function saveRobot(isContinue: boolean) {
|
||||
try {
|
||||
drawerLoading.value = true;
|
||||
const params = { ...robotForm.value };
|
||||
switch (robotForm.value.platform) {
|
||||
case 'WE_COM':
|
||||
case 'LARK':
|
||||
case 'CUSTOM':
|
||||
params.type = undefined;
|
||||
params.appKey = undefined;
|
||||
params.appSecret = undefined;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await updateRobot(params as RobotEditParams);
|
||||
Message.success(t('common.updateSuccess'));
|
||||
showDetailDrawer.value = false;
|
||||
} else {
|
||||
await addRobot(params as RobotAddParams);
|
||||
Message.success(t('common.addSuccess'));
|
||||
robotFormRef.value?.resetFields();
|
||||
robotForm.value = { ...defaultRobot };
|
||||
if (!isContinue) {
|
||||
showDetailDrawer.value = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
drawerLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理抽屉确认
|
||||
* @param isContinue 是否继续添加
|
||||
*/
|
||||
function handleDrawerConfirm(isContinue: boolean) {
|
||||
robotFormRef.value?.validate(async (errors: Record<string, ValidatedError> | undefined) => {
|
||||
if (!errors) {
|
||||
saveRobot(isContinue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleDrawerCancel() {
|
||||
showDetailDrawer.value = false;
|
||||
robotFormRef.value?.resetFields();
|
||||
robotForm.value = { ...defaultRobot };
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
createRobot: handleCreateClick,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.robot-list-container {
|
||||
@apply relative;
|
||||
.ms-container--shadow();
|
||||
.robot-list {
|
||||
@apply grid h-full overflow-y-auto;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 16px;
|
||||
grid-template-columns: repeat(2, minmax(128px, 1fr));
|
||||
border-radius: var(--border-radius-small);
|
||||
background-color: var(--color-text-n9);
|
||||
gap: 16px;
|
||||
.robot-card {
|
||||
@apply flex flex-col bg-white;
|
||||
|
||||
padding: 24px;
|
||||
max-height: 128px;
|
||||
border-radius: var(--border-radius-small);
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.platform-card,
|
||||
.platform-card-custom {
|
||||
@apply flex cursor-pointer items-center;
|
||||
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-small);
|
||||
gap: 8px;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 10px rgb(100 100 102 / 15%);
|
||||
}
|
||||
}
|
||||
.platform-card--active {
|
||||
border-color: rgb(var(--primary-5));
|
||||
}
|
||||
.platform-card-custom {
|
||||
@apply border-dashed;
|
||||
|
||||
border-width: 2px;
|
||||
gap: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<div> </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 项目管理-消息管理-编辑消息模板
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<MsTabCard v-model:active-tab="activeTab" :title="t('project.messageManagement')" :tab-list="tabList" />
|
||||
<MessageList v-if="activeTab === 'config'" @create-robot="createRobot" />
|
||||
<RobotList v-else-if="activeTab === 'botList'" ref="robotListRef" :active-tab="activeTab" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* @description 项目管理-消息管理
|
||||
*/
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import MsTabCard from '@/components/pure/ms-tab-card/index.vue';
|
||||
import RobotList from './components/robotList.vue';
|
||||
import MessageList from './components/messageList.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const activeTab = ref((route.query.tab as string) || 'config');
|
||||
const tabList = [
|
||||
{ key: 'config', title: t('project.messageManagement.config') },
|
||||
{ key: 'botList', title: t('project.messageManagement.botList') },
|
||||
];
|
||||
|
||||
const robotListRef = ref();
|
||||
|
||||
function createRobot() {
|
||||
activeTab.value = 'botList';
|
||||
nextTick(() => {
|
||||
robotListRef.value?.createRobot();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -0,0 +1 @@
|
|||
export default {};
|
|
@ -0,0 +1,60 @@
|
|||
export default {
|
||||
'project.messageManagement': '消息管理',
|
||||
'project.messageManagement.config': '消息设置',
|
||||
'project.messageManagement.botList': '机器人列表',
|
||||
'project.messageManagement.robot': '机器人',
|
||||
'project.messageManagement.botListTips':
|
||||
'机器人的开启与关闭状态与消息设置列表机器人同步;如:站内信开启,消息列表页则展示站内信通知列',
|
||||
'project.messageManagement.notRemind': '不再提醒',
|
||||
'project.messageManagement.createBot': '创建机器人',
|
||||
'project.messageManagement.createAt': '创建于',
|
||||
'project.messageManagement.updateAt': '更新于',
|
||||
'project.messageManagement.status': '状态',
|
||||
'project.messageManagement.statusTipOn': '开启:在消息通知列表展示及使用',
|
||||
'project.messageManagement.statusTipOff': '关闭:暂不使用改机器人',
|
||||
'project.messageManagement.choosePlatform': '选择配置平台',
|
||||
'project.messageManagement.WE_COM': '企业微信',
|
||||
'project.messageManagement.DING_TALK': '钉钉',
|
||||
'project.messageManagement.LARK': '飞书',
|
||||
'project.messageManagement.CUSTOM': '自定义',
|
||||
'project.messageManagement.business': '(企业版)',
|
||||
'project.messageManagement.name': '机器人名称',
|
||||
'project.messageManagement.nameRequired': '机器人名称不能为空',
|
||||
'project.messageManagement.namePlaceholder': '请输入机器人名称',
|
||||
'project.messageManagement.webhook': 'Webhook',
|
||||
'project.messageManagement.webhookRequired': 'Webhook 不能为空',
|
||||
'project.messageManagement.webhookPlaceholder': '在{type}群内,点击「机器人」可直接获取',
|
||||
'project.messageManagement.webhookCustomPlaceholder': '请输入 webhook',
|
||||
'project.messageManagement.dingTalkType': '机器人类型',
|
||||
'project.messageManagement.ENTERPRISE': '企业内部应用',
|
||||
'project.messageManagement.dingTalkCustomTip': '钉钉自定义机器人产品线下公告',
|
||||
'project.messageManagement.noticeDetail': '公告详情',
|
||||
'project.messageManagement.dingTalkCustomTitle': '添加钉钉自定义机器人场景注意事项',
|
||||
'project.messageManagement.dingTalkCustomContent1':
|
||||
'1. 若使用安全验证的钉钉机器人,请选择 “自定义关键词” 验证,关键词为 “消息通知”;',
|
||||
'project.messageManagement.dingTalkCustomContent2': '2. 若使用 {at} 功能,接收人必须是机器人所在群的用户;',
|
||||
'project.messageManagement.dingTalkCustomContent3': '3. 若使用手机通知,接收人手机号必须为钉钉企业所使用的手机号',
|
||||
'project.messageManagement.expand': '展开',
|
||||
'project.messageManagement.close': '收起',
|
||||
'project.messageManagement.dingTalkEnterpriseTip': '添加企业内部应用文档',
|
||||
'project.messageManagement.helpDoc': '帮助文档',
|
||||
'project.messageManagement.dingTalkEnterpriseTitle': '添加企业内部应用机器人场景注意事项',
|
||||
'project.messageManagement.dingTalkEnterpriseContent1': '1. 若使用 {at} 功能,接收人必须是机器人所在群的用户;',
|
||||
'project.messageManagement.dingTalkEnterpriseContent2': '2. 若使用手机通知,接收人手机号必须为钉钉企业所使用的手机号',
|
||||
'project.messageManagement.appKey': 'AppKey',
|
||||
'project.messageManagement.appKeyPlaceholder': '打开帮助文档可直接获取',
|
||||
'project.messageManagement.appKeyRequired': 'AppKey 不能为空',
|
||||
'project.messageManagement.appSecret': 'AppSecret',
|
||||
'project.messageManagement.appSecretPlaceholder': '打开帮助文档可直接获取',
|
||||
'project.messageManagement.appSecretRequired': 'AppSecret 不能为空',
|
||||
'project.messageManagement.disableTitle': '确定关闭 {name} 吗?',
|
||||
'project.messageManagement.disableContent': '关闭后,将不在接收站内信通知,且不在消息列表页展示',
|
||||
'project.messageManagement.disableConfirm': '确认关闭',
|
||||
'project.messageManagement.disableSuccess': '关闭成功',
|
||||
'project.messageManagement.enableTitle': '开启 {name}',
|
||||
'project.messageManagement.enableContent': '开启后,站内信则显示在消息设置列表,需要手动设置通知类型',
|
||||
'project.messageManagement.enableConfirm': '确认开启',
|
||||
'project.messageManagement.enableSuccess': '开启成功',
|
||||
'project.messageManagement.deleteTitle': '确认删除 {name} ?',
|
||||
'project.messageManagement.deleteContent': '删除机器人后将不再推送绑定的消息事件',
|
||||
};
|
|
@ -26,9 +26,9 @@
|
|||
</template>
|
||||
<template #operation="{ record }">
|
||||
<div class="flex flex-row flex-nowrap">
|
||||
<MsButton @click="showAuthDrawer(record)">{{ t('project.userGroup.viewAuth') }}</MsButton>
|
||||
<MsButton class="!mr-0" @click="showAuthDrawer(record)">{{ t('project.userGroup.viewAuth') }}</MsButton>
|
||||
<a-divider v-if="!record.internal" direction="vertical" />
|
||||
<MsButton v-if="!record.internal" status="danger" @click="handleDelete(record)">{{
|
||||
<MsButton v-if="!record.internal" class="!mr-0" status="danger" @click="handleDelete(record)">{{
|
||||
t('common.delete')
|
||||
}}</MsButton>
|
||||
</div>
|
||||
|
@ -112,7 +112,7 @@
|
|||
updateOrAddProjectUserGroup,
|
||||
} from '@/api/modules/project-management/usergroup';
|
||||
import UserDrawer from './userDrawer.vue';
|
||||
import MsButton from '@/components/pure/ms-button/not-mr.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import useModal from '@/hooks/useModal';
|
||||
import { Message, ValidatedError } from '@arco-design/web-vue';
|
||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<MsCard simple auto-height>
|
||||
<div class="filter-box">
|
||||
<div class="filter-item">
|
||||
<MsSearchSelect
|
||||
<MsSelect
|
||||
v-model:model-value="operUser"
|
||||
mode="remote"
|
||||
placeholder="system.log.operatorPlaceholder"
|
||||
|
@ -21,6 +21,7 @@
|
|||
:option-label-render="
|
||||
(item) => `${item.label}<span class='text-[var(--color-text-2)]'>(${item.email})</span>`
|
||||
"
|
||||
allow-search
|
||||
allow-clear
|
||||
/>
|
||||
</div>
|
||||
|
@ -142,7 +143,7 @@
|
|||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import { MENU_LEVEL } from '@/config/pathMap';
|
||||
import MsSearchSelect from '@/components/business/ms-search-select/index';
|
||||
import MsSelect from '@/components/business/ms-select/index';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
||||
import type { CascaderOption, SelectOptionData } from '@arco-design/web-vue';
|
||||
|
@ -439,6 +440,7 @@
|
|||
title: 'system.log.operateName',
|
||||
dataIndex: 'content',
|
||||
slotName: 'content',
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'system.log.time',
|
||||
|
@ -548,3 +550,4 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
@/components/business/ms-select/index
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
'system.log.operator': 'Operator',
|
||||
'system.log.operatorPlaceholder': 'Please enter username/email to search',
|
||||
'system.log.operatorPlaceholder': 'Enter username/email to search',
|
||||
'system.log.operateTime': 'Operation time',
|
||||
'system.log.operateRange': 'Operating range',
|
||||
'system.log.operateType': 'Operation type',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
'system.log.operator': '操作人',
|
||||
'system.log.operatorPlaceholder': '请输入用户名/邮箱搜索',
|
||||
'system.log.operatorPlaceholder': '输入用户名/邮箱搜索',
|
||||
'system.log.operateTime': '操作时间',
|
||||
'system.log.operateRange': '操作范围',
|
||||
'system.log.operateType': '操作类型',
|
||||
|
|
Loading…
Reference in New Issue