feat(消息管理): useSelect钩子&消息管理部分页面&部分组件调整
This commit is contained in:
parent
9404db0222
commit
bbccbe49be
|
@ -570,6 +570,194 @@ const fileList = [
|
||||||
updateTime: 18975439859,
|
updateTime: 18975439859,
|
||||||
createTime: 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>> {
|
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 {
|
.arco-checkbox-icon {
|
||||||
border: 1px solid var(--color-text-input-border);
|
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 {
|
.arco-checkbox-indeterminate .arco-checkbox-icon {
|
||||||
border-color: rgba(var(--primary-7));
|
border-color: rgba(var(--primary-7));
|
||||||
|
@ -407,8 +417,12 @@
|
||||||
.arco-dropdown,
|
.arco-dropdown,
|
||||||
.arco-trigger-menu,
|
.arco-trigger-menu,
|
||||||
.arco-select-dropdown {
|
.arco-select-dropdown {
|
||||||
|
padding: 6px;
|
||||||
border: 0.5px solid var(--color-text-n8);
|
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%);
|
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-dropdown-list,
|
||||||
.arco-select-dropdown-list,
|
.arco-select-dropdown-list,
|
||||||
.arco-trigger-menu-inner {
|
.arco-trigger-menu-inner {
|
||||||
|
@ -427,6 +441,18 @@
|
||||||
background-color: rgb(var(--primary-1));
|
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 {
|
.ms-dropdown-divider {
|
||||||
margin: 6px 0;
|
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 {
|
.arco-dropdown-option-content {
|
||||||
@apply flex items-center;
|
@apply flex items-center;
|
||||||
|
|
||||||
|
@ -742,3 +775,14 @@
|
||||||
white-space: nowrap;
|
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(
|
watch(
|
||||||
|
@ -219,7 +209,7 @@
|
||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
.ms-container--shadow();
|
.ms-container--shadow();
|
||||||
.ms-card-list {
|
.ms-card-list {
|
||||||
@apply grid overflow-auto;
|
@apply grid max-h-full overflow-auto;
|
||||||
|
|
||||||
.ms-scroll-bar();
|
.ms-scroll-bar();
|
||||||
|
|
||||||
|
|
|
@ -71,8 +71,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, Ref, nextTick, onMounted, computed, onBeforeUnmount } from 'vue';
|
import { ref, watch, Ref } from 'vue';
|
||||||
import { calculateMaxDepth } from '@/utils';
|
import useSelect from '@/hooks/useSelect';
|
||||||
|
|
||||||
import type { CascaderOption } from '@arco-design/web-vue';
|
import type { CascaderOption } from '@arco-design/web-vue';
|
||||||
import type { VirtualListProps } from '@arco-design/web-vue/es/_components/virtual-list-v2/interface';
|
import type { VirtualListProps } from '@arco-design/web-vue/es/_components/virtual-list-v2/interface';
|
||||||
|
@ -102,11 +102,15 @@
|
||||||
|
|
||||||
const innerValue = ref<CascaderModelValue>([]);
|
const innerValue = ref<CascaderModelValue>([]);
|
||||||
const innerLevel = ref(''); // 顶级选项,该级别为单选选项
|
const innerLevel = ref(''); // 顶级选项,该级别为单选选项
|
||||||
const maxTagCount = ref(1); // 最大显示 tag 数量
|
|
||||||
const cascader: Ref = ref(null);
|
const cascader: Ref = ref(null);
|
||||||
const cascaderWidth = ref(0); // cascader 宽度
|
|
||||||
const cascaderDeep = ref(1); // 默认层级只有一层
|
const { maxTagCount, getOptionComputedStyle, calculateMaxTag } = useSelect({
|
||||||
const cascaderViewInner = ref<HTMLElement | null>(null); // 输入框内容容器 DOM
|
selectRef: cascader,
|
||||||
|
selectVal: innerValue,
|
||||||
|
isCascade: true,
|
||||||
|
options: props.options,
|
||||||
|
panelWidth: props.panelWidth,
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => 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 {
|
interface CascaderValue {
|
||||||
level: keyof typeof props.levelTop;
|
level: keyof typeof props.levelTop;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -203,42 +186,12 @@
|
||||||
innerLevel.value = '';
|
innerLevel.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nextTick(() => {
|
calculateMaxTag();
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearValues() {
|
function clearValues() {
|
||||||
innerLevel.value = '';
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
@ -273,3 +226,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@/hooks/useSelect
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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 }}
|
{{ props.text }}
|
||||||
<MsIcon
|
<MsIcon
|
||||||
v-if="props.showFillIcon"
|
v-if="props.showFillIcon"
|
||||||
|
@ -7,6 +7,13 @@
|
||||||
class="cursor-pointer text-[rgb(var(--primary-6))]"
|
class="cursor-pointer text-[rgb(var(--primary-6))]"
|
||||||
@click="fillHeapByDefault"
|
@click="fillHeapByDefault"
|
||||||
></MsIcon>
|
></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -17,12 +24,14 @@
|
||||||
defineProps<{
|
defineProps<{
|
||||||
text: string;
|
text: string;
|
||||||
showFillIcon?: boolean;
|
showFillIcon?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
iconText?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
showFillIcon: true,
|
showFillIcon: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emit = defineEmits(['fill']);
|
const emit = defineEmits(['fill', 'iconClick']);
|
||||||
|
|
||||||
function fillHeapByDefault() {
|
function fillHeapByDefault() {
|
||||||
emit('fill');
|
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 { debounce } from 'lodash-es';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import useSelect from '@/hooks/useSelect';
|
||||||
|
|
||||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
@ -15,11 +16,14 @@ export type RemoteFieldsMap = {
|
||||||
export interface MsSearchSelectProps {
|
export interface MsSearchSelectProps {
|
||||||
mode?: 'static' | 'remote'; // 静态模式,远程模式。默认为静态模式,需要传入 options 数据;远程模式需要传入请求函数
|
mode?: 'static' | 'remote'; // 静态模式,远程模式。默认为静态模式,需要传入 options 数据;远程模式需要传入请求函数
|
||||||
modelValue: ModelType;
|
modelValue: ModelType;
|
||||||
|
allowSearch?: boolean;
|
||||||
allowClear?: boolean;
|
allowClear?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
prefix?: string;
|
hasAllSelect?: boolean; // 是否有全选选项
|
||||||
searchKeys: string[]; // 需要搜索的 key 名,关键字会遍历这个 key 数组,然后取 item[key] 进行模糊匹配
|
searchKeys?: string[]; // 需要搜索的 key 名,关键字会遍历这个 key 数组,然后取 item[key] 进行模糊匹配
|
||||||
|
valueKey?: string; // 选项的 value 字段名,默认为 value
|
||||||
options: SelectOptionData[];
|
options: SelectOptionData[];
|
||||||
|
multiple?: boolean; // 是否多选
|
||||||
remoteFieldsMap?: RemoteFieldsMap; // 远程模式下的结果 key 映射,例如 { value: 'id' },表示远程请求时,会将返回结果的 id 赋值到 value 字段
|
remoteFieldsMap?: RemoteFieldsMap; // 远程模式下的结果 key 映射,例如 { value: 'id' },表示远程请求时,会将返回结果的 id 赋值到 value 字段
|
||||||
remoteExtraParams?: Record<string, any>; // 远程模式下的额外参数
|
remoteExtraParams?: Record<string, any>; // 远程模式下的额外参数
|
||||||
remoteFunc?(params: Record<string, any>): Promise<any>; // 远程模式下的请求函数,返回一个 Promise
|
remoteFunc?(params: Record<string, any>): Promise<any>; // 远程模式下的请求函数,返回一个 Promise
|
||||||
|
@ -27,13 +31,20 @@ export interface MsSearchSelectProps {
|
||||||
optionTooltipContent?: (item: SelectOptionData) => string; // 自定义 option 的 tooltip 内容,返回一个字符串,默认使用 item.label
|
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(
|
export default defineComponent(
|
||||||
(props: MsSearchSelectProps, { emit }) => {
|
(props: MsSearchSelectProps & MsSearchSelectSlots, { emit, slots }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const innerValue = ref(props.modelValue);
|
const innerValue = ref(props.modelValue);
|
||||||
const filterOptions = ref<SelectOptionData[]>([]);
|
const filterOptions = ref<SelectOptionData[]>([]); // 实际渲染的 options,会根据搜索关键字进行过滤
|
||||||
const remoteOriginOptions = ref<SelectOptionData[]>([...props.options]);
|
const remoteOriginOptions = ref<SelectOptionData[]>([...props.options]); // 远程模式下的原始 options,接口返回的数据会存储在这里
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(val) => {
|
(val) => {
|
||||||
|
@ -41,12 +52,14 @@ export default defineComponent(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
const selectRef = ref<Ref>();
|
||||||
() => props.options,
|
|
||||||
(arr) => {
|
const { maxTagCount, getOptionComputedStyle, calculateMaxTag } = useSelect({
|
||||||
filterOptions.value = [...arr];
|
selectRef,
|
||||||
}
|
selectVal: innerValue,
|
||||||
);
|
isCascade: true,
|
||||||
|
options: props.options,
|
||||||
|
});
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
@ -76,7 +89,11 @@ export default defineComponent(
|
||||||
}
|
}
|
||||||
if (val.trim() === '') {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const highlightedKeyword = `<span class="text-[rgb(var(--primary-4))]">${val}</span>`;
|
const highlightedKeyword = `<span class="text-[rgb(var(--primary-4))]">${val}</span>`;
|
||||||
|
@ -84,6 +101,7 @@ export default defineComponent(
|
||||||
.map((e) => {
|
.map((e) => {
|
||||||
const item = { ...e };
|
const item = { ...e };
|
||||||
let hasMatch = false;
|
let hasMatch = false;
|
||||||
|
if (props.searchKeys) {
|
||||||
for (let i = 0; i < props.searchKeys.length; i++) {
|
for (let i = 0; i < props.searchKeys.length; i++) {
|
||||||
// 遍历传入的搜索字段
|
// 遍历传入的搜索字段
|
||||||
const key = props.searchKeys[i];
|
const key = props.searchKeys[i];
|
||||||
|
@ -93,13 +111,16 @@ export default defineComponent(
|
||||||
item[key] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
item[key] = e[key].replace(new RegExp(val, 'gi'), highlightedKeyword); // 高亮关键字替换
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (hasMatch) {
|
if (hasMatch) {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter((e) => e) as SelectOptionData[];
|
.filter((e) => e) as SelectOptionData[];
|
||||||
|
calculateMaxTag();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -111,31 +132,92 @@ export default defineComponent(
|
||||||
? h('div', { innerHTML: props.optionLabelRender(item) })
|
? h('div', { innerHTML: props.optionLabelRender(item) })
|
||||||
: item.label;
|
: 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(() => {
|
onBeforeMount(() => {
|
||||||
handleUserSearch('');
|
handleUserSearch('');
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<a-select
|
<a-select
|
||||||
|
ref={selectRef}
|
||||||
default-value={innerValue}
|
default-value={innerValue}
|
||||||
placeholder={t(props.placeholder || '')}
|
placeholder={t(props.placeholder || '')}
|
||||||
allow-clear={props.allowClear}
|
allow-clear={props.allowClear}
|
||||||
allow-search
|
allow-search={props.allowSearch}
|
||||||
filter-option={false}
|
filter-option={false}
|
||||||
loading={loading.value}
|
loading={loading.value}
|
||||||
|
multiple={props.multiple}
|
||||||
|
max-tag-count={maxTagCount.value}
|
||||||
onUpdate:model-value={(value: ModelType) => emit('update:modelValue', value)}
|
onUpdate:model-value={(value: ModelType) => emit('update:modelValue', value)}
|
||||||
onInputValueChange={debounce(handleUserSearch, 300)}
|
onInputValueChange={debounce(handleUserSearch, 300)}
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
prefix: () => t(props.prefix || ''),
|
prefix: () => t(props.prefix || ''),
|
||||||
default: () =>
|
...selectSlots(),
|
||||||
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>
|
|
||||||
)),
|
|
||||||
}}
|
}}
|
||||||
</a-select>
|
</a-select>
|
||||||
);
|
);
|
||||||
|
@ -145,16 +227,20 @@ export default defineComponent(
|
||||||
props: [
|
props: [
|
||||||
'mode',
|
'mode',
|
||||||
'modelValue',
|
'modelValue',
|
||||||
|
'allowSearch',
|
||||||
'allowClear',
|
'allowClear',
|
||||||
'placeholder',
|
'placeholder',
|
||||||
'prefix',
|
|
||||||
'searchKeys',
|
'searchKeys',
|
||||||
|
'valueKey',
|
||||||
'options',
|
'options',
|
||||||
'optionLabelRender',
|
'optionLabelRender',
|
||||||
'remoteFieldsMap',
|
'remoteFieldsMap',
|
||||||
'remoteExtraParams',
|
'remoteExtraParams',
|
||||||
'remoteFunc',
|
'remoteFunc',
|
||||||
'optionTooltipContent',
|
'optionTooltipContent',
|
||||||
|
'prefix',
|
||||||
|
'hasAllSelect',
|
||||||
|
'multiple',
|
||||||
],
|
],
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
}
|
}
|
|
@ -39,7 +39,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
font-size: 1rem;
|
font-size: 14px;
|
||||||
border-radius: var(--border-radius-mini);
|
border-radius: var(--border-radius-mini);
|
||||||
line-height: 22px;
|
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',
|
'ms-card',
|
||||||
'relative',
|
'relative',
|
||||||
'h-full',
|
'h-full',
|
||||||
|
props.isFullscreen ? 'ms-card--no-radius' : '',
|
||||||
props.autoHeight ? '' : 'min-h-[500px]',
|
props.autoHeight ? '' : 'min-h-[500px]',
|
||||||
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
|
props.noContentPadding ? 'ms-card--noContentPadding' : 'p-[24px]',
|
||||||
]"
|
]"
|
||||||
|
@ -15,17 +16,8 @@
|
||||||
</div>
|
</div>
|
||||||
<a-divider v-if="!props.simple" class="mb-[16px]" />
|
<a-divider v-if="!props.simple" class="mb-[16px]" />
|
||||||
<div class="ms-card-container">
|
<div class="ms-card-container">
|
||||||
<a-scrollbar
|
<a-scrollbar class="pr-[5px]" :style="getComputedContentStyle">
|
||||||
class="pr-[5px]"
|
<div class="relative h-full w-full" :style="{ minWidth: `${props.minWidth || 1000}px` }">
|
||||||
: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` }">
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</a-scrollbar>
|
</a-scrollbar>
|
||||||
|
@ -72,10 +64,11 @@
|
||||||
specialHeight: number; // 特殊高度,例如某些页面有面包屑
|
specialHeight: number; // 特殊高度,例如某些页面有面包屑
|
||||||
hideBack: boolean; // 隐藏返回按钮
|
hideBack: boolean; // 隐藏返回按钮
|
||||||
autoHeight: boolean; // 内容区域高度是否自适应
|
autoHeight: boolean; // 内容区域高度是否自适应
|
||||||
otherWidth?: number; // 该宽度为卡片外部同级容器的宽度
|
otherWidth: number; // 该宽度为卡片外部同级容器的宽度
|
||||||
minWidth?: number; // 卡片最小宽度
|
minWidth: number; // 卡片最小宽度
|
||||||
hasBreadcrumb: boolean; // 是否有面包屑,如果有面包屑,高度需要减去面包屑的高度
|
hasBreadcrumb: boolean; // 是否有面包屑,如果有面包屑,高度需要减去面包屑的高度
|
||||||
noContentPadding: boolean; // 内容区域是否有padding
|
noContentPadding: boolean; // 内容区域是否有padding
|
||||||
|
isFullscreen?: boolean; // 是否全屏
|
||||||
handleBack: () => void; // 自定义返回按钮触发事件
|
handleBack: () => void; // 自定义返回按钮触发事件
|
||||||
}>
|
}>
|
||||||
>(),
|
>(),
|
||||||
|
@ -117,6 +110,23 @@
|
||||||
return 246 + _specialHeight;
|
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() {
|
function back() {
|
||||||
if (typeof props.handleBack === 'function') {
|
if (typeof props.handleBack === 'function') {
|
||||||
props.handleBack();
|
props.handleBack();
|
||||||
|
@ -128,7 +138,7 @@
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.ms-card {
|
.ms-card {
|
||||||
@apply overflow-hidden bg-white;
|
@apply relative overflow-hidden bg-white;
|
||||||
|
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
box-shadow: 0 0 10px rgb(120 56 135 / 5%);
|
box-shadow: 0 0 10px rgb(120 56 135 / 5%);
|
||||||
|
@ -158,4 +168,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ms-card--no-radius {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -44,6 +44,9 @@
|
||||||
</a-scrollbar>
|
</a-scrollbar>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<slot name="footer">
|
<slot name="footer">
|
||||||
|
<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">
|
<a-button :disabled="props.okLoading" @click="handleCancel">
|
||||||
{{ t(props.cancelText || 'ms.drawer.cancel') }}
|
{{ t(props.cancelText || 'ms.drawer.cancel') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
@ -53,6 +56,8 @@
|
||||||
<a-button type="primary" :loading="props.okLoading" @click="handleOk">
|
<a-button type="primary" :loading="props.okLoading" @click="handleOk">
|
||||||
{{ t(props.okText || 'ms.drawer.ok') }}
|
{{ t(props.okText || 'ms.drawer.ok') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
|
|
|
@ -154,14 +154,6 @@
|
||||||
initScrollListener();
|
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,
|
immediate: true,
|
||||||
|
|
|
@ -84,12 +84,6 @@
|
||||||
listContent.addEventListener('scroll', listenScroll);
|
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() {
|
function handleReachBottom() {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="left-side">
|
<div class="left-side">
|
||||||
<a-space>
|
<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]" />
|
<img :src="props.logo" class="mr-[4px] h-[32px] w-[32px]" />
|
||||||
{{ props.name }}
|
{{ props.name }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -225,6 +225,20 @@ export const pathMap: PathMapItem[] = [
|
||||||
permission: [],
|
permission: [],
|
||||||
level: MENU_LEVEL[2],
|
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 {
|
export enum ProjectManagementRouteEnum {
|
||||||
PROJECT_MANAGEMENT = 'projectManagement',
|
PROJECT_MANAGEMENT = 'projectManagement',
|
||||||
PROJECT_MANAGEMENT_FILE_MANAGEMENT = 'projectManagementFileManageMent',
|
PROJECT_MANAGEMENT_FILE_MANAGEMENT = 'projectManagementFileManageMent',
|
||||||
|
PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT = 'projectManagementMessageManagement',
|
||||||
|
PROJECT_MANAGEMENT_MESSAGE_MANAGEMENT_EDIT = 'projectManagementMessageManagementEdit',
|
||||||
PROJECT_MANAGEMENT_LOG = 'projectManagementLog',
|
PROJECT_MANAGEMENT_LOG = 'projectManagementLog',
|
||||||
PROJECT_MANAGEMENT_PERMISSION = 'projectManagementPermission',
|
PROJECT_MANAGEMENT_PERMISSION = 'projectManagementPermission',
|
||||||
PROJECT_MANAGEMENT_PERMISSION_BASIC_INFO = 'projectManagementPermissionBasicInfo',
|
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 滚动事件
|
* @param event 滚动事件
|
||||||
|
@ -38,16 +45,14 @@ export default function useContainerShadow(options: ContainerShadowOptions) {
|
||||||
function listenScroll(event: Event) {
|
function listenScroll(event: Event) {
|
||||||
if (event.target) {
|
if (event.target) {
|
||||||
const listContent = event.target as HTMLElement;
|
const listContent = event.target as HTMLElement;
|
||||||
const { scrollTop, scrollHeight, clientHeight } = listContent;
|
calculateArrivedPosition(listContent);
|
||||||
const scrollBottom = scrollHeight - clientHeight - scrollTop;
|
|
||||||
isArrivedTop.value = scrollTop < options.overHeight;
|
|
||||||
isArrivedBottom.value = scrollBottom < options.overHeight;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initScrollListener() {
|
function initScrollListener() {
|
||||||
if (!isInitListener.value && containerRef.value) {
|
if (!isInitListener.value && containerRef.value) {
|
||||||
containerRef.value.addEventListener('scroll', listenScroll);
|
containerRef.value.addEventListener('scroll', listenScroll);
|
||||||
|
calculateArrivedPosition(containerRef.value); // 初始化计算一次,因为初始化的时候内容可能超出可视区域了
|
||||||
isInitListener.value = true;
|
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.internal': 'Internal',
|
||||||
'common.custom': 'Custom',
|
'common.custom': 'Custom',
|
||||||
'common.preview': 'Preview',
|
'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.performanceTest': 'Performance Test',
|
||||||
'menu.projectManagement': 'Project',
|
'menu.projectManagement': 'Project',
|
||||||
'menu.projectManagement.fileManagement': 'File Management',
|
'menu.projectManagement.fileManagement': 'File Management',
|
||||||
|
'menu.projectManagement.messageManagement': 'Message Management',
|
||||||
|
'menu.projectManagement.messageManagementEdit': 'Update Template',
|
||||||
'menu.featureTest.featureCase': 'Feature Case',
|
'menu.featureTest.featureCase': 'Feature Case',
|
||||||
'meun.workstation': 'Workstation',
|
'meun.workstation': 'Workstation',
|
||||||
'menu.loadTest': 'Performance Test',
|
'menu.loadTest': 'Performance Test',
|
||||||
|
|
|
@ -57,4 +57,7 @@ export default {
|
||||||
'common.internal': '内部',
|
'common.internal': '内部',
|
||||||
'common.custom': '自定义',
|
'common.custom': '自定义',
|
||||||
'common.preview': '预览',
|
'common.preview': '预览',
|
||||||
|
'common.fullScreen': '全屏',
|
||||||
|
'common.offFullScreen': '退出全屏',
|
||||||
|
'common.allSelect': '全选',
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,6 +31,8 @@ export default {
|
||||||
'menu.projectManagement': '项目管理',
|
'menu.projectManagement': '项目管理',
|
||||||
'menu.projectManagement.log': '日志',
|
'menu.projectManagement.log': '日志',
|
||||||
'menu.projectManagement.fileManagement': '文件管理',
|
'menu.projectManagement.fileManagement': '文件管理',
|
||||||
|
'menu.projectManagement.messageManagement': '消息管理',
|
||||||
|
'menu.projectManagement.messageManagementEdit': '更新模板',
|
||||||
'menu.featureTest.featureCase': '功能用例',
|
'menu.featureTest.featureCase': '功能用例',
|
||||||
'menu.projectManagement.projectPermission': '项目与权限',
|
'menu.projectManagement.projectPermission': '项目与权限',
|
||||||
'menu.settings': '系统设置',
|
'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,
|
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',
|
path: 'log',
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<a-input
|
<a-input
|
||||||
v-else
|
v-else
|
||||||
v-model:model-value="form.field"
|
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')"
|
:placeholder="props.fieldConfig?.placeholder || t('project.fileManagement.namePlaceholder')"
|
||||||
class="w-[245px]"
|
class="w-[245px]"
|
||||||
@press-enter="beforeConfirm(undefined)"
|
@press-enter="beforeConfirm(undefined)"
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeMount, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* @description 项目管理-文件管理
|
||||||
|
*/
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useI18n } from '@/hooks/useI18n';
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
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>
|
||||||
<template #operation="{ record }">
|
<template #operation="{ record }">
|
||||||
<div class="flex flex-row flex-nowrap">
|
<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" />
|
<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')
|
t('common.delete')
|
||||||
}}</MsButton>
|
}}</MsButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
updateOrAddProjectUserGroup,
|
updateOrAddProjectUserGroup,
|
||||||
} from '@/api/modules/project-management/usergroup';
|
} from '@/api/modules/project-management/usergroup';
|
||||||
import UserDrawer from './userDrawer.vue';
|
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 useModal from '@/hooks/useModal';
|
||||||
import { Message, ValidatedError } from '@arco-design/web-vue';
|
import { Message, ValidatedError } from '@arco-design/web-vue';
|
||||||
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<MsCard simple auto-height>
|
<MsCard simple auto-height>
|
||||||
<div class="filter-box">
|
<div class="filter-box">
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
<MsSearchSelect
|
<MsSelect
|
||||||
v-model:model-value="operUser"
|
v-model:model-value="operUser"
|
||||||
mode="remote"
|
mode="remote"
|
||||||
placeholder="system.log.operatorPlaceholder"
|
placeholder="system.log.operatorPlaceholder"
|
||||||
|
@ -21,6 +21,7 @@
|
||||||
:option-label-render="
|
:option-label-render="
|
||||||
(item) => `${item.label}<span class='text-[var(--color-text-2)]'>(${item.email})</span>`
|
(item) => `${item.label}<span class='text-[var(--color-text-2)]'>(${item.email})</span>`
|
||||||
"
|
"
|
||||||
|
allow-search
|
||||||
allow-clear
|
allow-clear
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -142,7 +143,7 @@
|
||||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
import { MENU_LEVEL } from '@/config/pathMap';
|
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 useAppStore from '@/store/modules/app';
|
||||||
|
|
||||||
import type { CascaderOption, SelectOptionData } from '@arco-design/web-vue';
|
import type { CascaderOption, SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
@ -439,6 +440,7 @@
|
||||||
title: 'system.log.operateName',
|
title: 'system.log.operateName',
|
||||||
dataIndex: 'content',
|
dataIndex: 'content',
|
||||||
slotName: 'content',
|
slotName: 'content',
|
||||||
|
showTooltip: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'system.log.time',
|
title: 'system.log.time',
|
||||||
|
@ -548,3 +550,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@/components/business/ms-select/index
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
'system.log.operator': 'Operator',
|
'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.operateTime': 'Operation time',
|
||||||
'system.log.operateRange': 'Operating range',
|
'system.log.operateRange': 'Operating range',
|
||||||
'system.log.operateType': 'Operation type',
|
'system.log.operateType': 'Operation type',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
'system.log.operator': '操作人',
|
'system.log.operator': '操作人',
|
||||||
'system.log.operatorPlaceholder': '请输入用户名/邮箱搜索',
|
'system.log.operatorPlaceholder': '输入用户名/邮箱搜索',
|
||||||
'system.log.operateTime': '操作时间',
|
'system.log.operateTime': '操作时间',
|
||||||
'system.log.operateRange': '操作范围',
|
'system.log.operateRange': '操作范围',
|
||||||
'system.log.operateType': '操作类型',
|
'system.log.operateType': '操作类型',
|
||||||
|
|
Loading…
Reference in New Issue