diff --git a/dbm-ui/frontend/.eslintignore b/dbm-ui/frontend/.eslintignore
index 541a2a38d7..55042961bd 100644
--- a/dbm-ui/frontend/.eslintignore
+++ b/dbm-ui/frontend/.eslintignore
@@ -2,3 +2,4 @@ node_modules/
dist/
public/
src/types/auto-imports.d.ts
+patch/
diff --git a/dbm-ui/frontend/package.json b/dbm-ui/frontend/package.json
index ff381b9dbc..b3ed0da68d 100644
--- a/dbm-ui/frontend/package.json
+++ b/dbm-ui/frontend/package.json
@@ -18,6 +18,7 @@
"@blueking/bk-weweb": "0.0.15",
"@blueking/bkflow.js": "0.1.10",
"@blueking/ip-selector": "0.0.1-beta.126",
+ "@icon-cool/bk-icon-bk-biz-components": "0.0.4",
"@vueuse/core": "10.2.1",
"axios": "1.2.1",
"bkui-vue": "0.0.2-beta.68",
diff --git a/dbm-ui/frontend/patch/user-selector/alternate-item.tsx b/dbm-ui/frontend/patch/user-selector/alternate-item.tsx
new file mode 100644
index 0000000000..ebeafc51ef
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/alternate-item.tsx
@@ -0,0 +1,120 @@
+// @ts-nocheck
+import { computed, defineComponent, toRefs, withModifiers } from 'vue';
+
+import RenderAvatar from './render-avatar';
+import RenderList from './render-list';
+import tooltips from './tooltips';
+
+export default defineComponent({
+ name: 'AlternateItem',
+ directives: {
+ tooltips,
+ },
+ // props: ['selector', 'user', 'keyword', 'index'],
+ props: {
+ selector: {
+ type: Object,
+ },
+ user: {
+ type: Object,
+ },
+ keyword: {
+ type: String,
+ },
+ index: {
+ type: Number,
+ },
+ },
+ setup(props) {
+ const { selector, user, keyword } = toRefs(props);
+ const disabled = computed(() => selector.value.disabledUsers.includes(user.value.username));
+ const getItemContent = () => {
+ const [nameWithoutDomain, domain] = user.value.username.split('@');
+ let displayText = nameWithoutDomain;
+ if (keyword.value) {
+ displayText = displayText.replace(
+ new RegExp(keyword.value, 'g'),
+ `${keyword.value}`,
+ );
+ }
+ const displayUsername = selector.value.displayDomain && domain
+ ? `${displayText}@${domain}`
+ : displayText;
+
+ const displayName = user.value.display_name;
+ if (displayName) {
+ return `${displayUsername}(${displayName})`;
+ }
+
+ return displayUsername;
+ };
+ const getTitle = () => selector.value.getDisplayText(user.value);
+
+ return {
+ disabled,
+ getItemContent,
+ getTitle,
+ };
+ },
+ render() {
+ return (
+
e.stopPropagation()}
+ onMousedown={withModifiers(() => this.selector.handleUserMousedown(this.user, this.disabled), ['left', 'stop'])}
+ onMouseup={withModifiers(() => this.selector.handleUserMouseup(this.user, this.disabled), ['left', 'stop'])}>
+ {
+ this.selector.renderList
+ ? <>
+
+
+ >
+ : <>
+ {
+ this.selector.tagType === 'avatar'
+ ? <>
+
+
+ >
+ : null
+ }
+ {
+ this.selector.displayListTips && this.user.category_name
+ ? <>
+
+ { this.user.category_name }
+
+ >
+ : null
+ }
+
+ >
+ }
+
+ );
+ },
+});
diff --git a/dbm-ui/frontend/patch/user-selector/alternate-list.tsx b/dbm-ui/frontend/patch/user-selector/alternate-list.tsx
new file mode 100644
index 0000000000..fdb08056ed
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/alternate-list.tsx
@@ -0,0 +1,165 @@
+/* eslint-disable no-prototype-builtins */
+
+import { hideAll } from 'tippy.js';
+import {
+ type ComponentPublicInstance,
+ computed,
+ defineComponent,
+ getCurrentInstance,
+ type HTMLAttributes,
+ nextTick,
+ ref,
+ watch,
+ withModifiers,
+} from 'vue';
+
+import AlternateItem from './alternate-item';
+import instanceStore from './instance-store';
+
+export default defineComponent({
+ setup() {
+ const { proxy } = getCurrentInstance();
+ instanceStore.setInstance('alternateContent', 'alternateList', proxy);
+
+ const selector = ref(null);
+ const keyword = ref('');
+ const next = ref(true);
+ const loading = ref(true);
+ const matchedUsers = ref([]);
+ const wrapperStyle = computed(() => {
+ const style: any = {};
+ if (selector.value?.panelWidth) {
+ style.width = `${parseInt(selector.value.panelWidth, 10)}px`;
+ }
+ return style;
+ });
+ const listStyle = computed(() => {
+ const style = {
+ 'max-height': '192px',
+ };
+ if (selector.value) {
+ const maxHeight = parseInt(selector.value.listScrollHeight, 10);
+ if (!isNaN(maxHeight)) {
+ style['max-height'] = `${maxHeight}px`;
+ }
+ }
+ return style;
+ });
+ const getIndex = (index: number, childIndex = 0) => {
+ let flattenedIndex = 0;
+ matchedUsers.value.slice(0, index).forEach((user) => {
+ if (user.hasOwnProperty('children')) {
+ flattenedIndex += user.children.length;
+ } else {
+ flattenedIndex += 1;
+ }
+ });
+ return flattenedIndex + childIndex;
+ };
+
+ const handleScroll = () => {
+ hideAll({ exclude: selector.value.inputRef, duration: 0 });
+ if (loading.value || !next.value) {
+ return false;
+ }
+ const list = alternateList.value;
+ const threshold = 32;
+ if (list.scrollTop + list.clientHeight > list.scrollHeight - threshold) {
+ selector.value.search(keyword.value, next.value);
+ }
+ };
+
+ watch(keyword, () => {
+ alternateItem.value = [];
+ nextTick(() => {
+ alternateList.value.scrollTop = 0;
+ });
+ });
+
+ const alternateListContainer = ref(null);
+ const alternateList = ref(null);
+ const alternateItem = ref([]);
+
+ const setRef = (el: HTMLElement | ComponentPublicInstance | HTMLAttributes) => {
+ alternateItem.value.push(el);
+ };
+
+ return {
+ selector,
+ keyword,
+ next,
+ loading,
+ matchedUsers,
+ wrapperStyle,
+ listStyle,
+ getIndex,
+ handleScroll,
+ alternateListContainer,
+ alternateList,
+ alternateItem,
+ setRef,
+ };
+ },
+ render() {
+ return (
+
+
void this.handleScroll()}>
+ {
+ this.matchedUsers.map((user, index) => {
+ if (user.hasOwnProperty('children')) {
+ return <>
+ - e.stopPropagation()}
+ onMousedown={withModifiers((): any => void this.selector.handleGroupMousedown(), ['left', 'stop'])}
+ onMouseup={withModifiers((): any => void this.selector.handleGroupMouseup(), ['left', 'stop'])}>
+ { `${user.display_name}(${user.children.length})` }
+
+ {
+ user.children.map((child: any, childIndex: number) => <>
+ void this.setRef(el)}
+ index={this.getIndex(index, childIndex)}
+ selector={this.selector}
+ user={child}
+ keyword={this.keyword} />
+ >)
+ }
+ >;
+ }
+ return <>
+ void this.setRef(el)}
+ selector={this.selector}
+ user={user}
+ index={this.getIndex(index)}
+ keyword={this.keyword} />
+ >;
+ })
+ }
+
+ {
+ (!this.loading && !this.matchedUsers.length)
+ ? <>
+
+ { this.selector.emptyText }
+
+ >
+ : null
+ }
+
+ );
+ },
+});
diff --git a/dbm-ui/frontend/patch/user-selector/icon-user.svg b/dbm-ui/frontend/patch/user-selector/icon-user.svg
new file mode 100644
index 0000000000..4bdf4f533d
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/icon-user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/dbm-ui/frontend/patch/user-selector/index.ts b/dbm-ui/frontend/patch/user-selector/index.ts
new file mode 100644
index 0000000000..250dbc4e80
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/index.ts
@@ -0,0 +1,31 @@
+import type { App, Plugin } from 'vue';
+
+import request from './request';
+import UserSelector from './selector.vue';
+
+// UserSelector.install = (Vue) => {
+// window.$vueApp.component(UserSelector.name, UserSelector);
+// };
+
+// export default UserSelector;
+
+// export { request };
+
+export interface OriginComponent {
+ name: string;
+ install?: Plugin;
+}
+
+const withInstall = (component: T): T & Plugin => {
+ // eslint-disable-next-line no-param-reassign
+ component.install = function (app: App, { prefix } = {}) {
+ const pre = app.config.globalProperties.bkUIPrefix || prefix || 'Bk';
+ app.component(pre + component.name, component);
+ };
+ return component as T & Plugin;
+};
+
+export { request };
+
+const BkUserSelector = withInstall(UserSelector);
+export default BkUserSelector;
diff --git a/dbm-ui/frontend/patch/user-selector/instance-store.ts b/dbm-ui/frontend/patch/user-selector/instance-store.ts
new file mode 100644
index 0000000000..fedc963480
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/instance-store.ts
@@ -0,0 +1,16 @@
+import { type ComponentPublicInstance } from 'vue';
+
+const store = {};
+
+export default {
+ setInstance(group: string, id: string, proxy: ComponentPublicInstance) {
+ if (!store[group]) {
+ store[group] = {};
+ }
+
+ store[group][id] = proxy;
+ },
+ getInstance(group: string, id: string) {
+ return store[group][id];
+ },
+};
diff --git a/dbm-ui/frontend/patch/user-selector/render-alternate.ts b/dbm-ui/frontend/patch/user-selector/render-alternate.ts
new file mode 100644
index 0000000000..3d7cc39a91
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/render-alternate.ts
@@ -0,0 +1,20 @@
+// import * as Vue from 'vue';
+// export default {
+// name: 'render-alternate',
+// data() {
+// return { selector: null };
+// },
+// render() {
+// return this.selector.defaultAlternate(Vue.h);
+// },
+// };
+
+import { h } from 'vue';
+
+export default {
+ name: 'render-alternate',
+ props: ['selector'],
+ setup(props: any): any {
+ return () => props.selector.defaultAlternate(h);
+ },
+};
diff --git a/dbm-ui/frontend/patch/user-selector/render-avatar.tsx b/dbm-ui/frontend/patch/user-selector/render-avatar.tsx
new file mode 100644
index 0000000000..886945aaa1
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/render-avatar.tsx
@@ -0,0 +1,51 @@
+// export default {
+// name: 'render-avatar',
+// props: ['user', 'urlMethod'],
+// render() {
+// return ;
+// },
+// async created() {
+// try {
+// let avatar = null;
+// if (typeof this.user === 'string') {
+// avatar = await this.urlMethod(this.user);
+// } else if (typeof this.user === 'object') {
+// avatar = this.user.avatar
+// || this.user.logo
+// || (await this.urlMethod(this.user.username));
+// }
+// if (avatar) {
+// this.$el.style.backgroundImage = `url(${avatar})`;
+// }
+// } catch (e) {}
+// },
+// };
+
+import { onMounted, ref, watch } from 'vue';
+
+export default {
+ name: 'render-avatar',
+ props: ['user', 'urlMethod'],
+ setup(props: any) {
+ const avatar = ref('');
+ onMounted(async () => {
+ try {
+ if (typeof props.user === 'string') {
+ avatar.value = await props.urlMethod(props.user);
+ } else if (typeof props.user === 'object') {
+ avatar.value = props.user.avatar || props.user.logo || (await props.urlMethod(props.user.username));
+ }
+ } catch (e) {}
+ });
+
+ const userSelectorAvatarRef = ref(null);
+
+ watch(avatar, (v: string) => {
+ if (v) {
+ userSelectorAvatarRef.value.style.backgroundImage = `url(${avatar.value})`;
+ }
+ });
+
+ return () => ;
+ },
+};
diff --git a/dbm-ui/frontend/patch/user-selector/render-list.ts b/dbm-ui/frontend/patch/user-selector/render-list.ts
new file mode 100644
index 0000000000..2712e201f8
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/render-list.ts
@@ -0,0 +1,43 @@
+import { h } from 'vue';
+
+// export default {
+// name: 'render-list',
+// props: ['selector', 'user', 'index', 'keyword', 'disabled'],
+// render() {
+// return this.selector.renderList(h, {
+// user: this.user,
+// index: this.index,
+// keyword: this.keyword,
+// disabled: this.disabled,
+// });
+// },
+// };
+
+export default {
+ name: 'render-list',
+ props: {
+ selector: {
+ type: Object,
+ },
+ user: {
+ type: Object,
+ },
+ keyword: {
+ type: String,
+ },
+ index: {
+ type: Number,
+ },
+ disabled: {
+ type: Boolean,
+ },
+ },
+ setup(props: any): any {
+ return () => props.selector.renderList(h, {
+ user: props.user,
+ index: props.index,
+ keyword: props.keyword,
+ disabled: props.disabled,
+ });
+ },
+};
diff --git a/dbm-ui/frontend/patch/user-selector/render-tag.ts b/dbm-ui/frontend/patch/user-selector/render-tag.ts
new file mode 100644
index 0000000000..113f18216d
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/render-tag.ts
@@ -0,0 +1,44 @@
+// import * as Vue from 'vue';
+// export default {
+// name: 'render-tag',
+// props: ['username', 'user', 'index'],
+// render() {
+// return this.$parent.renderTag(Vue.h, {
+// username: this.username,
+// index: this.index,
+// user: this.user,
+// });
+// },
+// };
+
+import { h, inject } from 'vue';
+
+export default {
+ name: 'render-tag',
+ props: ['username', 'user', 'index'],
+ // props: {
+ // selector: {
+ // type: Object,
+ // },
+ // user: {
+ // type: Object,
+ // },
+ // keyword: {
+ // type: String,
+ // },
+ // index: {
+ // type: Number,
+ // },
+ // disabled: {
+ // type: Boolean,
+ // },
+ // },
+ setup(props: any) {
+ const parentSelector = inject('parentSelector');
+ return () => (parentSelector as any).renderTag(h, {
+ username: props.username,
+ index: props.index,
+ user: props.user,
+ });
+ },
+};
diff --git a/dbm-ui/frontend/patch/user-selector/request.ts b/dbm-ui/frontend/patch/user-selector/request.ts
new file mode 100644
index 0000000000..7fea1455fc
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/request.ts
@@ -0,0 +1,222 @@
+// @ts-nocheck
+
+import { createApp, getCurrentInstance, ref, watch } from 'vue';
+
+import instanceStore from './instance-store';
+
+let callbackSeed = 0;
+function JSONP(api: string, params = {}, options: any = {}) {
+ return new Promise((resolve, reject) => {
+ let timer: number;
+ const callbackName = `USER_LIST_CALLBACK_${callbackSeed += 1}`;
+ window[callbackName] = (response: any) => {
+ timer && clearTimeout(timer);
+ document.body.removeChild(script);
+ delete window[callbackName];
+ resolve(response);
+ };
+ const script = document.createElement('script');
+ script.onerror = (_event) => {
+ document.body.removeChild(script);
+ delete window[callbackName];
+ reject('Get user list failed.');
+ };
+ const query = [];
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in params) {
+ query.push(`${key}=${params[key]}`);
+ }
+ script.src = `${api}?${query.join('&')}&callback=${callbackName}`;
+ if (options.timeout) {
+ setTimeout(() => {
+ document.body.removeChild(script);
+ delete window[callbackName];
+ reject('Get user list timeout.');
+ }, options.timeout);
+ }
+ document.body.appendChild(script);
+ });
+}
+
+// 缓存已经加载过的人员
+// 以api为key,存储不同数据源的用户
+const userMap = new Map();
+
+function getMap(api: string) {
+ if (userMap.has(api)) {
+ return userMap.get(api);
+ }
+ const map = new Map();
+ userMap.set(api, map);
+ return map;
+}
+
+function storeUsers(api: string, users: any) {
+ const map = getMap(api);
+ users.forEach((user: any) => map.set(user.username, user));
+}
+
+function getUsers(api: string, usernames: any) {
+ const map = getMap(api);
+ const users: string[] = [];
+ usernames.forEach((username: string) => {
+ if (map.has(username)) {
+ users.push(map.get(username));
+ }
+ });
+ return users;
+}
+
+// 接口最大支持100条记录,超过100条需要将请求拆分为多个
+async function handleBatchSearch(api: string, usernames: string[], options: any) {
+ const map = getMap(api);
+ const unique = [...new Set(usernames)].filter(username => !map.has(username));
+ if (!unique.length) {
+ return Promise.resolve(getUsers(api, usernames));
+ }
+ const slices: string[][] = [];
+ unique.reduce((slice, username, index) => {
+ if (slice.length < 100) {
+ slice.push(username);
+ if (index === unique.length - 1) {
+ slices.push(slice);
+ }
+ return slice;
+ }
+ slices.push(slice);
+ return [];
+ }, []);
+ try {
+ const responses = await Promise.all(slices.map(slice => JSONP(
+ api,
+ {
+ app_code: 'bk-magicbox',
+ exact_lookups: slice.join(','),
+ page_size: 100,
+ page: 1,
+ },
+ options,
+ )));
+ responses.forEach((response: any) => {
+ if (response.code !== 0) return;
+ storeUsers(api, response.data.results || []);
+ });
+ } catch (error) {
+ console.error(error);
+ }
+ return Promise.resolve(getUsers(api, usernames));
+}
+
+function createVm(apiStr: string) {
+ const app = {
+ setup() {
+ const { proxy } = getCurrentInstance();
+ instanceStore.setInstance('exactSearch', apiStr, proxy);
+
+ const api = ref('');
+ api.value = apiStr;
+
+ const queue = ref([]);
+
+ watch(() => queue, (q: any) => {
+ q.value.length && dispatchSeach();
+ }, { deep: true });
+
+ const search = usernames => new Promise((resolve) => {
+ queue.value.push({
+ resolve,
+ usernames,
+ });
+ });
+
+ const dispatchSeach = async () => {
+ const currentQueue = [...queue.value];
+ queue.value = [];
+ try {
+ const allNames = currentQueue.reduce(
+ (all, { usernames }) => all.concat(usernames),
+ [],
+ );
+ const users = await request.exactSearch(api.value, allNames);
+ const map = {};
+ users.forEach((user) => {
+ map[user.username] = user;
+ });
+ currentQueue.forEach(({ resolve, usernames }) => {
+ const resolveData = [];
+ usernames.forEach((username) => {
+ // eslint-disable-next-line no-prototype-builtins
+ if (map.hasOwnProperty(username)) {
+ resolveData.push(map[username]);
+ }
+ });
+ resolve(resolveData);
+ });
+ } catch (error) {
+ currentQueue.forEach(({ resolve }) => {
+ resolve([]);
+ });
+ console.error(error);
+ }
+ };
+
+ return {
+ api,
+ queue,
+ search,
+ dispatchSeach,
+ };
+ },
+ };
+
+ const vm = createApp(app);
+ const vmContainer = document.createElement('div');
+ vm.mount(vmContainer);
+ // document.body.appendChild(vmContainer);
+ // vmMap.set(apiStr, vm);
+ return vm;
+}
+
+const request = {
+ // 模糊搜索,失败时返回空
+ async fuzzySearch(api, params, options) {
+ const data = {};
+ try {
+ const response = await JSONP(api, params, options);
+ if (response.code !== 0) {
+ throw new Error(response);
+ }
+ data.count = response.data.count;
+ data.results = response.data.results || [];
+ storeUsers(api, data.results);
+ } catch (error) {
+ console.error(error.message);
+ data.count = 0;
+ data.results = [];
+ }
+ return data;
+ },
+ // 精确搜索,不在此处捕获异常,在上层捕获并显示在tooltips中
+ async exactSearch(api, username, options) {
+ const isArray = Array.isArray(username);
+ const usernames = isArray ? username : [username];
+ const users = await handleBatchSearch(api, usernames, options);
+ if (isArray) {
+ return users;
+ }
+ return users[0];
+ },
+ // 粘贴时对粘贴的用户进行校验
+ pasteValidate(api, usernames, options) {
+ return handleBatchSearch(api, usernames, options);
+ },
+ // 队列式查询,用于多个组件共存时,批量拉取已存在的用户信息
+ scheduleExactSearch(api, username) {
+ const usernames = Array.isArray(username) ? username : [username];
+ createVm(api);
+ const vm = instanceStore.getInstance('exactSearch', api);
+ return vm.search(usernames);
+ },
+};
+
+export default request;
diff --git a/dbm-ui/frontend/patch/user-selector/selector.vue b/dbm-ui/frontend/patch/user-selector/selector.vue
new file mode 100644
index 0000000000..6a2ff9f538
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/selector.vue
@@ -0,0 +1,1198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getDisplayText(user) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ userInfo }}
+
+
+
+
+
+
diff --git a/dbm-ui/frontend/patch/user-selector/style.css b/dbm-ui/frontend/patch/user-selector/style.css
new file mode 100644
index 0000000000..947e99f1cc
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/style.css
@@ -0,0 +1,398 @@
+.user-selector * {
+ padding: 0;
+ margin: 0;
+ box-sizing: border-box;
+}
+
+.user-selector {
+ display: inline-block;
+ min-width: 120px;
+ font-size: 14px;
+ color: #63656e;
+ cursor: text;
+}
+
+.user-selector.user-selector-info {
+ display: inline;
+ min-width: initial;
+ color: inherit;
+}
+
+.user-selector .user-selector-layout {
+ position: relative;
+ height: 100%;
+}
+
+.user-selector .user-selector-layout .user-selector-container {
+ position: relative;
+ min-width: 100%;
+ min-height: 100%;
+ padding: 0 9px 0 3px;
+ overflow: hidden;
+ font-size: 0;
+ line-height: 1;
+ background-color: #fff;
+ border: 1px solid #c4c6cc;
+ border-radius: 2px;
+}
+
+.user-selector .user-selector-layout .user-selector-container.is-flex-height {
+ min-height: 32px;
+}
+
+.user-selector .user-selector-layout .user-selector-container.is-fast-clear {
+ padding-right: 22px;
+}
+
+.user-selector .user-selector-layout .user-selector-container.disabled {
+ cursor: not-allowed;
+ background-color: #fafbfd !important;
+ border-color: #dcdee5 !important;
+}
+
+.user-selector .user-selector-layout .user-selector-container.focus {
+ z-index: 1;
+ overflow: auto;
+ overflow-x: hidden;
+ overflow-y: auto;
+ white-space: normal;
+ border-color: #3a84ff;
+}
+
+.user-selector .user-selector-layout .user-selector-container.focus::-webkit-scrollbar {
+ width: 4px;
+ height: 4px;
+}
+
+.user-selector .user-selector-layout .user-selector-container.focus::-webkit-scrollbar-thumb {
+ background: #c4c6cc;
+ border-radius: 2px;
+ box-shadow: inset 0 0 6px rgb(204 204 204 / 30%);
+}
+
+.user-selector .user-selector-layout .user-selector-container.placeholder::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ padding: 0 0 0 10px;
+ font-size: 12px;
+ line-height: 30px;
+ color: #c3cdd7;
+ content: attr(data-placeholder);
+}
+
+.user-selector .user-selector-layout .user-selector-container.has-avatar .user-selector-selected {
+ padding: 0;
+ margin: 4px 5px;
+ background: transparent;
+}
+
+.user-selector .user-selector-layout .user-selector-container.has-avatar .user-selector-overflow-tag {
+ border-radius: 10px;
+}
+
+.user-selector .user-selector-layout .user-selector-container.is-loading::before {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ background-color: rgb(255 255 255 / 70%);
+ content: "";
+}
+
+.user-selector .user-selector-layout .user-selector-container.is-loading::after {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 3;
+ width: 6px;
+ height: 6px;
+ margin-left: -24px;
+ border-radius: 50%;
+ content: "";
+ box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1;
+ animation: user-selector-loading 1s linear infinite;
+}
+
+.user-selector .user-selector-layout .user-selector-overflow-count {
+ height: 22px;
+ min-width: 22px;
+ padding: 0 4px;
+ font-size: 12px;
+ line-height: 22px;
+ color: #63656e;
+ text-align: center;
+ background-color: #f0f1f5;
+}
+
+.user-selector .user-selector-layout .user-selector-clear {
+ position: absolute;
+ top: 10px;
+ right: 5px;
+ z-index: 1;
+ font-size: 12px;
+ color: #c4c6cc;
+ cursor: pointer;
+}
+
+.user-selector .user-selector-layout .user-selector-clear:hover {
+ color: #979ba5;
+}
+
+.user-selector .user-selector-selected {
+ display: inline-flex;
+ max-width: 100%;
+ padding: 0 2px 0 4px;
+ margin: 4px 0 4px 6px;
+ font-size: 12px;
+ line-height: 22px;
+ vertical-align: top;
+ cursor: pointer;
+ background: #f0f1f5;
+ border-radius: 2px;
+ outline: 0;
+ align-items: center;
+}
+
+.user-selector .user-selector-selected:hover {
+ background: #dcdee5;
+}
+
+.user-selector .user-selector-selected .user-selector-selected-value {
+ overflow: hidden;
+ font-size: 12px;
+ color: #63656e;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.user-selector .user-selector-selected .user-selector-selected-clear {
+ height: 18px;
+ font-size: 18px;
+ line-height: 18px;
+ color: #979ba5;
+ text-align: center;
+ cursor: pointer;
+ flex: 18px 0 0;
+}
+
+.user-selector .user-selector-selected .user-selector-selected-clear:hover {
+ color: #63656e;
+}
+
+.user-selector .user-selector-input {
+ display: inline-block;
+ height: 22px;
+ max-width: 100%;
+ padding: 0 0 0 6px;
+ margin: 4px 0 0;
+ overflow: hidden;
+ font-size: 12px;
+ line-height: 22px;
+ white-space: nowrap;
+ vertical-align: top;
+ outline: none;
+}
+
+.user-selector .user-selector-overflow-tag {
+ display: inline-flex;
+ min-width: 22px;
+ padding: 0 5px;
+ margin: 4px 0 4px 6px;
+ font-size: 12px;
+ line-height: 22px;
+ text-align: center;
+ background: #f0f1f5;
+}
+
+.user-selector .user-selector-overflow-tag ~ .user-selector-selected {
+ pointer-events: none;
+ visibility: hidden;
+}
+
+.user-selector .alternate-empty {
+ height: 32px;
+ padding: 0;
+ margin: 0;
+ line-height: 32px;
+ text-align: center;
+}
+
+@keyframes user-selector-loading {
+ 0% {
+ box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1;
+ }
+
+ 14% {
+ box-shadow: 12px 0 0 1px #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1;
+ }
+
+ 28% {
+ box-shadow: 12px 0 0 2px #fd6154, 24px 0 0 1px #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1;
+ }
+
+ 42% {
+ box-shadow: 12px 0 0 1px #fd6154, 24px 0 0 2px #ffb726, 36px 0 0 1px #4cd084, 48px 0 0 0 #57a3f1;
+ }
+
+ 56% {
+ box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 1px #ffb726, 36px 0 0 2px #4cd084, 48px 0 0 1px #57a3f1;
+ }
+
+ 70% {
+ box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 1px #4cd084, 48px 0 0 2px #57a3f1;
+ }
+
+ 84% {
+ box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 1px #57a3f1;
+ }
+}
+
+.user-selector-alternate-list-wrapper {
+ position: relative;
+ width: 190px;
+ color: #63656e;
+ background-color: #fff;
+}
+
+.user-selector-alternate-list-wrapper.has-folder {
+ width: 300px;
+}
+
+.user-selector-alternate-list-wrapper.is-loading {
+ min-height: 32px;
+}
+
+.user-selector-alternate-list-wrapper.is-loading::before {
+ position: absolute;
+ z-index: 1;
+ width: 100%;
+ height: 100%;
+ background-color: rgb(255 255 255 / 70%);
+ content: '';
+}
+
+.user-selector-alternate-list-wrapper.is-loading::after {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 6px;
+ height: 6px;
+ margin-left: -30px;
+ background-color: transparent;
+ border-radius: 50%;
+ content: '';
+ box-shadow: 12px 0 0 0 #fd6154, 24px 0 0 0 #ffb726, 36px 0 0 0 #4cd084, 48px 0 0 0 #57a3f1;
+ animation: user-selector-loading 1s linear infinite;
+}
+
+.user-selector-alternate-list-wrapper .alternate-list {
+ max-height: 162px;
+ padding: 0;
+ margin: 0;
+ overflow-y: auto;
+ font-size: 12px;
+ line-height: 32px;
+ background: #fff;
+}
+
+.user-selector-alternate-list-wrapper .alternate-list::-webkit-scrollbar {
+ width: 4px;
+ height: 4px;
+}
+
+.user-selector-alternate-list-wrapper .alternate-list::-webkit-scrollbar-thumb {
+ background: #c4c6cc;
+ border-radius: 2px;
+ box-shadow: inset 0 0 6px rgb(204 204 204 / 30%);
+}
+
+.user-selector-alternate-list-wrapper .alternate-item {
+ padding: 0 10px;
+ justify-content: space-between;
+ cursor: pointer;
+}
+
+.user-selector-alternate-list-wrapper .alternate-item.highlight, .user-selector-alternate-list-wrapper .alternate-item:hover {
+ background-color: #f1f7ff;
+}
+
+.user-selector-alternate-list-wrapper .alternate-item.disabled {
+ cursor: not-allowed;
+ opacity: 50%;
+}
+
+.user-selector-alternate-list-wrapper .alternate-item .item-avatar {
+ float: left;
+ margin: 5px 8px 0 0;
+}
+
+.user-selector-alternate-list-wrapper .alternate-item .item-name {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.user-selector-alternate-list-wrapper .alternate-item .item-name span {
+ color: #3a84ff;
+}
+
+.user-selector-alternate-list-wrapper .alternate-item .item-folder {
+ float: right;
+ max-width: 140px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ outline: 0;
+}
+
+.user-selector-alternate-list-wrapper .alternate-group {
+ padding: 0 11px;
+ overflow: hidden;
+ color: #979ba5;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.user-selector-alternate-list-wrapper .alternate-empty {
+ padding: 0;
+ margin: 0;
+ font-size: 12px;
+ line-height: 44px;
+ text-align: center;
+}
+
+.user-selector-avatar {
+ display: inline-block;
+ width: 22px;
+ height: 22px;
+ background-color: #eff0f5;
+ background-image: url('./icon-user.svg');
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+ border-radius: 50%;
+}
+
+.tippy-box[data-theme~="light"] {
+ font-size: 12px;
+ line-height: 24px;
+ color: #63656e;
+ border: 1px solid #dcdee5;
+ border-radius: 2px;
+ box-shadow: 0 2px 6px 0 rgb(0 0 0 / 10%);
+}
+
+.tippy-box[data-theme~="light"][data-theme~="user-selected-tips"], .tippy-box[data-theme~="light"][data-theme~="list-item-tips"] {
+ padding: 0 9px;
+}
+
+.tippy-box[data-theme~="light"][data-theme~="small-arrow"] > .tippy-arrow::before {
+ transform: scale(0.75);
+}
+
+.tippy-content {
+ padding: 0;
+}
diff --git a/dbm-ui/frontend/patch/user-selector/tooltips.ts b/dbm-ui/frontend/patch/user-selector/tooltips.ts
new file mode 100644
index 0000000000..56fe85926a
--- /dev/null
+++ b/dbm-ui/frontend/patch/user-selector/tooltips.ts
@@ -0,0 +1,29 @@
+import Tippy, { type ReferenceElement } from 'tippy.js';
+import type { DirectiveBinding } from 'vue';
+
+import 'tippy.js/dist/tippy.css';
+import 'tippy.js/themes/light.css';
+export default {
+ mounted(el: ReferenceElement, binding: DirectiveBinding) {
+ const props = typeof binding.value === 'object'
+ ? binding.value
+ : { disabled: false, content: binding.value };
+ const instance = Tippy(
+ el,
+ Object.assign({ appendTo: document.body }, props),
+ );
+ if (props.disabled) {
+ instance.disable();
+ }
+ },
+ unmounted(el: ReferenceElement) {
+ el._tippy?.destroy();
+ },
+ updated(el: ReferenceElement, binding: DirectiveBinding) {
+ const props = typeof binding.value === 'object'
+ ? binding.value
+ : { disabled: false, content: binding.value };
+ props.disabled ? el._tippy.disable() : el._tippy.enable();
+ el._tippy.setContent(props.content);
+ },
+};
diff --git a/dbm-ui/frontend/src/common/importComps.ts b/dbm-ui/frontend/src/common/importComps.ts
index ac330079af..af8c543991 100644
--- a/dbm-ui/frontend/src/common/importComps.ts
+++ b/dbm-ui/frontend/src/common/importComps.ts
@@ -31,6 +31,7 @@ import MoreActionExtend from '@components/more-action-extend/Index.vue';
import SmartAction from '@components/smart-action/index.vue';
import { ipSelector } from '@components/vue2/ip-selector';
+import UserSelector from '@patch/user-selector/selector.vue';
export const setGlobalComps = (app: App) => {
app.component('DbCard', DbCard);
@@ -50,4 +51,5 @@ export const setGlobalComps = (app: App) => {
app.component('I18nT', Translation);
app.component('FunController', FunController);
app.component('MoreActionExtend', MoreActionExtend);
+ app.component('UserSelector', UserSelector);
};
diff --git a/dbm-ui/frontend/src/services/monitorAlarm.ts b/dbm-ui/frontend/src/services/monitorAlarm.ts
index b2fafbfff3..802e62f436 100644
--- a/dbm-ui/frontend/src/services/monitorAlarm.ts
+++ b/dbm-ui/frontend/src/services/monitorAlarm.ts
@@ -18,11 +18,13 @@ import type { ListBase } from '@services/types/common';
interface AlarmGroup {
id: number,
name: string,
+ create_at: string,
+ creator: string,
updater: string,
update_at: string,
bk_biz_id: number,
monitor_group_id: number,
- related_policy_count: number,
+ used_count: number,
group_type: string,
db_type: string,
receivers: AlarmGroupRecivers[],
diff --git a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue
index 424f48e58a..38a8181405 100644
--- a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue
+++ b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/Index.vue
@@ -32,7 +32,8 @@
ref="tableRef"
class="alert-group-table"
:columns="columns"
- :data-source="getAlarmGroupList" />
+ :data-source="getAlarmGroupList"
+ :row-class="setRowClass" />
+
+
diff --git a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue
index 04d56b9016..8b0cf2fac7 100644
--- a/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue
+++ b/dbm-ui/frontend/src/views/monitor-alarm-db/alarm-group/components/RenderRow.vue
@@ -46,13 +46,11 @@
{{ item.displayName }}
-
+ width="400">
+
+{{ overflowData.length }}
diff --git a/dbm-ui/frontend/vite.config.ts b/dbm-ui/frontend/vite.config.ts
index 3e5cb69c7b..7a4892eb8e 100644
--- a/dbm-ui/frontend/vite.config.ts
+++ b/dbm-ui/frontend/vite.config.ts
@@ -51,6 +51,7 @@ export default defineConfig(({ mode }) => {
'@locales': resolve(__dirname, 'src/locales'),
'@images': resolve(__dirname, 'src/images'),
'@lib': resolve(__dirname, 'lib'),
+ '@patch': resolve(__dirname, 'patch'),
},
extensions: ['.tsx', '.ts', '.js'],
},