Files
cn-rdms-web/src/views/product/shared/components/product-team-batch-dialog.vue

1158 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { PRODUCT_MANAGER_ROLE_CODE } from '@/constants/business';
import { fetchGetDeptSimpleList, fetchGetUserManagementRelationTree } from '@/service/api';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { buildMenuTree } from '@/views/system/shared/menu-tree';
import IconEpUser from '~icons/ep/user';
import IconEpOfficeBuilding from '~icons/ep/office-building';
defineOptions({ name: 'ProductTeamBatchDialog' });
type Source = 'dept' | 'chain' | 'all';
export interface BatchMemberPayload {
userId: string;
roleId: string;
remark: string;
}
interface Props {
userOptions: Api.SystemManage.UserSimple[];
roleOptions: Api.SystemManage.RoleSimple[];
/** 已加入产品的 userId(包含产品经理 locked 行),作为禁选 */
disabledUserIds: readonly string[];
}
interface Emits {
(e: 'submit', payloads: BatchMemberPayload[]): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', { default: false });
const source = ref<Source>('dept');
const currentNodeId = ref<string | null>(null);
const selected = ref<Set<string>>(new Set());
const roleOverrides = ref<Map<string, string>>(new Map());
const hideAdded = ref(false);
const treeSearch = ref('');
const userSearch = ref('');
const defaultRoleId = ref('');
const batchRemark = ref('');
const deptTree = ref<Api.SystemManage.DeptSimple[]>([]);
const deptTreeLoading = ref(false);
let deptTreeLoaded = false;
async function ensureDeptTree() {
if (deptTreeLoaded) return;
deptTreeLoading.value = true;
const { data } = await fetchGetDeptSimpleList();
deptTreeLoading.value = false;
// 后端返回平铺数组(每条带 parentId),前端按 parentId 组装为嵌套树
deptTree.value = data ? buildMenuTree(data) : [];
deptTreeLoaded = true;
}
const chainTree = ref<Api.SystemManage.UserManagementRelationTreeRespVO[]>([]);
const chainTreeLoading = ref(false);
let chainTreeLoaded = false;
async function ensureChainTree() {
if (chainTreeLoaded) return;
chainTreeLoading.value = true;
const { data } = await fetchGetUserManagementRelationTree({ fromUserIndex: false });
chainTreeLoading.value = false;
chainTree.value = data ?? [];
chainTreeLoaded = true;
}
const disabledUserIdSet = computed(() => new Set(props.disabledUserIds.map(String)));
const selectableRoles = computed(() =>
props.roleOptions.filter(role => role.code !== PRODUCT_MANAGER_ROLE_CODE && role.visible !== 0)
);
const selectedCount = computed(() => selected.value.size);
const visibleSelectedIds = computed(() => [...selected.value].slice(0, 4));
const overflowSelectedCount = computed(() => Math.max(0, selected.value.size - 4));
const overflowPopoverVisible = ref(false);
const overflowReferenceEl = ref<HTMLElement | null>(null);
function handleOverflowOutsideClick(e: MouseEvent) {
if (!overflowPopoverVisible.value) return;
const target = e.target as HTMLElement | null;
if (!target) return;
// 点击在 popover 自身 / 任意 popper / dropdown 菜单 / reference 按钮上,都不关
if (target.closest('.team-batch__overflow-popper')) return;
if (target.closest('.el-popper')) return;
if (target.closest('.el-dropdown-menu')) return;
if (target.closest('.el-dropdown__popper')) return;
if (overflowReferenceEl.value?.contains(target)) return;
overflowPopoverVisible.value = false;
}
onMounted(() => document.addEventListener('mousedown', handleOverflowOutsideClick, true));
onBeforeUnmount(() => document.removeEventListener('mousedown', handleOverflowOutsideClick, true));
const overrideCount = computed(() => {
let count = 0;
for (const uid of roleOverrides.value.keys()) {
if (selected.value.has(uid)) count += 1;
}
return count;
});
const confirmDisabled = computed(() => selectedCount.value === 0 || !defaultRoleId.value);
function resetState() {
source.value = 'dept';
currentNodeId.value = null;
selected.value = new Set();
roleOverrides.value = new Map();
hideAdded.value = false;
treeSearch.value = '';
userSearch.value = '';
defaultRoleId.value = '';
batchRemark.value = '';
overflowPopoverVisible.value = false;
}
function collectDeptIds(node: Api.SystemManage.DeptSimple): string[] {
const ids: string[] = [String(node.id)];
if (node.children) {
for (const c of node.children) ids.push(...collectDeptIds(c));
}
return ids;
}
function getDeptUserIds(node: Api.SystemManage.DeptSimple): string[] {
const deptIds = new Set(collectDeptIds(node));
return props.userOptions
.filter(u => u.deptId !== null && u.deptId !== undefined && deptIds.has(String(u.deptId)))
.map(u => String(u.id));
}
type TreeCheckState = 'none' | 'partial' | 'all';
function getDeptCheckState(node: Api.SystemManage.DeptSimple): TreeCheckState {
const ids = getDeptUserIds(node).filter(id => !disabledUserIdSet.value.has(id));
if (!ids.length) return 'none';
const sel = ids.filter(id => selected.value.has(id)).length;
if (sel === 0) return 'none';
if (sel === ids.length) return 'all';
return 'partial';
}
function findDeptNode(list: Api.SystemManage.DeptSimple[], key: string): Api.SystemManage.DeptSimple | null {
for (const n of list) {
if (String(n.id) === key) return n;
if (n.children) {
const r = findDeptNode(n.children, key);
if (r) return r;
}
}
return null;
}
function chainNodeKey(node: Api.SystemManage.UserManagementRelationTreeRespVO): string {
return node.id ?? `chain_${node.userId}`;
}
function getChainUserIds(node: Api.SystemManage.UserManagementRelationTreeRespVO): string[] {
const ids = new Set<string>([String(node.userId)]);
if (node.children) {
for (const c of node.children) {
for (const id of getChainUserIds(c)) ids.add(id);
}
}
return [...ids];
}
function getChainCheckState(node: Api.SystemManage.UserManagementRelationTreeRespVO): TreeCheckState {
const ids = getChainUserIds(node).filter(id => !disabledUserIdSet.value.has(id));
if (!ids.length) return 'none';
const sel = ids.filter(id => selected.value.has(id)).length;
if (sel === 0) return 'none';
if (sel === ids.length) return 'all';
return 'partial';
}
function findChainNode(
list: Api.SystemManage.UserManagementRelationTreeRespVO[],
key: string
): Api.SystemManage.UserManagementRelationTreeRespVO | null {
for (const n of list) {
if (chainNodeKey(n) === key) return n;
if (n.children) {
const r = findChainNode(n.children, key);
if (r) return r;
}
}
return null;
}
function toggleChainCheck(node: Api.SystemManage.UserManagementRelationTreeRespVO) {
const ids = getChainUserIds(node).filter(id => !disabledUserIdSet.value.has(id));
const state = getChainCheckState(node);
if (state === 'all') {
for (const id of ids) {
selected.value.delete(id);
roleOverrides.value.delete(id);
}
} else {
for (const id of ids) selected.value.add(id);
}
selected.value = new Set(selected.value);
roleOverrides.value = new Map(roleOverrides.value);
}
function activateNode(key: string) {
currentNodeId.value = key;
}
function handleDeptNodeClick(data: Api.SystemManage.DeptSimple) {
activateNode(String(data.id));
}
function handleChainNodeClick(data: Api.SystemManage.UserManagementRelationTreeRespVO) {
activateNode(chainNodeKey(data));
}
function toggleDeptCheck(node: Api.SystemManage.DeptSimple) {
const ids = getDeptUserIds(node).filter(id => !disabledUserIdSet.value.has(id));
const state = getDeptCheckState(node);
if (state === 'all') {
for (const id of ids) {
selected.value.delete(id);
roleOverrides.value.delete(id);
}
} else {
for (const id of ids) selected.value.add(id);
}
selected.value = new Set(selected.value);
roleOverrides.value = new Map(roleOverrides.value);
}
const deptTreeProps = { children: 'children', label: 'name' } as const;
const chainTreeProps = { children: 'children', label: 'userNickname' } as const;
function getDeptCheckStateClass(node: Api.SystemManage.DeptSimple) {
const s = getDeptCheckState(node);
return { 'is-checked': s === 'all', 'is-partial': s === 'partial' };
}
function getDeptMetaText(node: Api.SystemManage.DeptSimple): string {
const total = getDeptUserIds(node).length;
return total > 0 ? `${total}` : '';
}
function getChainCheckStateClass(node: Api.SystemManage.UserManagementRelationTreeRespVO) {
const s = getChainCheckState(node);
return { 'is-checked': s === 'all', 'is-partial': s === 'partial' };
}
function getChainMetaText(node: Api.SystemManage.UserManagementRelationTreeRespVO): string {
const total = getChainUserIds(node).length;
return total > 1 ? `${total}` : '';
}
function visibleUserIds(): string[] {
if (source.value === 'all') return props.userOptions.map(u => String(u.id));
if (!currentNodeId.value) return props.userOptions.map(u => String(u.id));
if (source.value === 'dept') {
const node = findDeptNode(deptTree.value, currentNodeId.value);
return node ? getDeptUserIds(node) : props.userOptions.map(u => String(u.id));
}
if (source.value === 'chain') {
const node = findChainNode(chainTree.value, currentNodeId.value);
return node ? getChainUserIds(node) : props.userOptions.map(u => String(u.id));
}
return props.userOptions.map(u => String(u.id));
}
function matchDeptKeyword(node: Api.SystemManage.DeptSimple, kw: string): boolean {
if (!kw) return true;
if (node.name.toLowerCase().includes(kw)) return true;
if (node.children) return node.children.some(c => matchDeptKeyword(c, kw));
return false;
}
function matchChainKeyword(node: Api.SystemManage.UserManagementRelationTreeRespVO, kw: string): boolean {
if (!kw) return true;
if (node.userNickname.toLowerCase().includes(kw)) return true;
if (node.children) return node.children.some(c => matchChainKeyword(c, kw));
return false;
}
const visibleDeptTree = computed(() => {
const kw = treeSearch.value.trim().toLowerCase();
if (!kw) return deptTree.value;
return deptTree.value.filter(n => matchDeptKeyword(n, kw));
});
const visibleChainTree = computed(() => {
const kw = treeSearch.value.trim().toLowerCase();
if (!kw) return chainTree.value;
return chainTree.value.filter(n => matchChainKeyword(n, kw));
});
const filteredUserIds = computed(() => {
let ids = visibleUserIds();
if (hideAdded.value) ids = ids.filter(id => !disabledUserIdSet.value.has(id));
const kw = userSearch.value.trim().toLowerCase();
if (kw) {
ids = ids.filter(id => {
const u = getUserById(id);
if (!u) return false;
return u.nickname.toLowerCase().includes(kw) || (u.username ?? '').toLowerCase().includes(kw);
});
}
return ids;
});
async function switchSource(next: Source) {
if (source.value === next) return;
source.value = next;
currentNodeId.value = null;
treeSearch.value = '';
if (next === 'dept') await ensureDeptTree();
else if (next === 'chain') await ensureChainTree();
}
function toggleUser(uid: string) {
if (disabledUserIdSet.value.has(uid)) return;
if (selected.value.has(uid)) {
selected.value.delete(uid);
roleOverrides.value.delete(uid);
} else {
selected.value.add(uid);
}
selected.value = new Set(selected.value);
roleOverrides.value = new Map(roleOverrides.value);
}
function clearAll() {
selected.value = new Set();
roleOverrides.value = new Map();
}
function getUserById(uid: string) {
return props.userOptions.find(u => String(u.id) === uid);
}
function getRoleNameById(roleId: string): string {
return props.roleOptions.find(r => r.id === roleId)?.name ?? '';
}
function setRoleOverride(uid: string, roleId: string | null) {
if (roleId === null) roleOverrides.value.delete(uid);
else roleOverrides.value.set(uid, roleId);
roleOverrides.value = new Map(roleOverrides.value);
}
function clearUserFilter() {
userSearch.value = '';
hideAdded.value = false;
}
async function handleConfirm() {
if (confirmDisabled.value) return;
const payloads: BatchMemberPayload[] = [...selected.value].map(uid => ({
userId: uid,
roleId: roleOverrides.value.get(uid) ?? defaultRoleId.value,
remark: batchRemark.value.trim()
}));
emit('submit', payloads);
}
watch(visible, async value => {
if (!value) {
resetState();
return;
}
if (source.value === 'dept') await ensureDeptTree();
else if (source.value === 'chain') await ensureChainTree();
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
title="新增团队成员"
preset="lg"
width="820px"
max-body-height="540px"
:confirm-disabled="confirmDisabled"
:confirm-text="`确认添加(${selectedCount})`"
@confirm="handleConfirm"
>
<div class="team-batch">
<div class="team-batch__tabs">
<button
v-for="tab in [
{ key: 'dept', label: '部门' },
{ key: 'chain', label: '团队' },
{ key: 'all', label: '全部用户' }
]"
:key="tab.key"
class="team-batch__tab"
:class="{ 'is-active': source === tab.key }"
type="button"
@click="switchSource(tab.key as Source)"
>
{{ tab.label }}
</button>
</div>
<div class="team-batch__picker" :class="{ 'is-single': source === 'all' }">
<!-- 左栏:dept / chain 树,all 时隐藏 -->
<div v-if="source !== 'all'" class="team-batch__col team-batch__col--tree">
<div class="team-batch__col-head">{{ source === 'dept' ? '部门' : '团队' }}</div>
<div class="team-batch__search">
<ElInput
v-model="treeSearch"
size="small"
clearable
:placeholder="source === 'dept' ? '搜索部门…' : '搜索成员…'"
/>
</div>
<div v-loading="source === 'dept' ? deptTreeLoading : chainTreeLoading" class="team-batch__col-body">
<ElTree
v-if="source === 'dept'"
:data="visibleDeptTree"
:props="deptTreeProps"
node-key="id"
:expand-on-click-node="false"
:default-expand-all="true"
:indent="14"
class="team-batch__tree"
@node-click="handleDeptNodeClick"
>
<template #default="{ data }">
<div class="team-batch__node" :class="{ 'is-active': currentNodeId === String(data.id) }">
<span
class="team-batch__node-check"
:class="getDeptCheckStateClass(data)"
@click.stop="toggleDeptCheck(data)"
/>
<IconEpOfficeBuilding class="team-batch__node-icon" />
<span class="team-batch__node-label">{{ data.name }}</span>
<span v-if="getDeptMetaText(data)" class="team-batch__node-meta">
{{ getDeptMetaText(data) }}
</span>
</div>
</template>
</ElTree>
<ElTree
v-else
:data="visibleChainTree"
:props="chainTreeProps"
node-key="userId"
:expand-on-click-node="false"
:default-expand-all="true"
:indent="14"
class="team-batch__tree"
@node-click="handleChainNodeClick"
>
<template #default="{ data }">
<div class="team-batch__node" :class="{ 'is-active': currentNodeId === chainNodeKey(data) }">
<span
class="team-batch__node-check"
:class="getChainCheckStateClass(data)"
@click.stop="toggleChainCheck(data)"
/>
<IconEpUser class="team-batch__node-icon" />
<span class="team-batch__node-label">{{ data.userNickname }}</span>
<span v-if="getChainMetaText(data)" class="team-batch__node-meta">
{{ getChainMetaText(data) }}
</span>
</div>
</template>
</ElTree>
</div>
</div>
<!-- 右栏:候选用户 -->
<div class="team-batch__col team-batch__col--users">
<div class="team-batch__col-head team-batch__col-head--user">
<span>
候选用户(
<span>{{ filteredUserIds.length }}</span>
人)
</span>
<label class="team-batch__hide-added">
<ElCheckbox v-model="hideAdded">隐藏已添加</ElCheckbox>
</label>
</div>
<div class="team-batch__search">
<ElInput v-model="userSearch" size="small" clearable placeholder="搜索用户名 / 部门…" />
</div>
<div class="team-batch__col-body">
<div v-if="!filteredUserIds.length" class="team-batch__empty">
该节点下没有匹配用户
<button
v-if="userSearch || hideAdded"
type="button"
class="team-batch__link team-batch__empty-action"
@click="clearUserFilter"
>
清除筛选条件
</button>
</div>
<div
v-for="uid in filteredUserIds"
:key="uid"
class="team-batch__user-row"
:class="{ 'is-disabled': disabledUserIdSet.has(uid) }"
@click="toggleUser(uid)"
>
<span class="team-batch__node-check" :class="{ 'is-checked': selected.has(uid) }" />
<span class="team-batch__user-avatar">{{ (getUserById(uid)?.nickname ?? '?').slice(0, 1) }}</span>
<div class="team-batch__user-main">
<div class="team-batch__user-name">{{ getUserById(uid)?.nickname }}</div>
</div>
<span v-if="disabledUserIdSet.has(uid)" class="team-batch__user-tag">已添加</span>
</div>
</div>
</div>
</div>
<div class="team-batch__selected">
<div class="team-batch__selected-head">
<span>
已选
<strong>{{ selectedCount }}</strong>
<span v-if="overrideCount > 0">
· 其中
<strong class="team-batch__override-cnt">{{ overrideCount }}</strong>
人已自定义角色
</span>
</span>
<button type="button" class="team-batch__link team-batch__link--danger" @click="clearAll">清空</button>
</div>
<div v-if="selectedCount === 0" class="team-batch__selected-empty">从左侧勾选用户后会出现在这里</div>
<div v-else class="team-batch__chips">
<span v-for="uid in visibleSelectedIds" :key="uid" class="team-batch__chip">
<span class="team-batch__chip-name">{{ getUserById(uid)?.nickname }}</span>
<ElDropdown
trigger="click"
@command="roleId => setRoleOverride(uid, roleId === '__default__' ? null : (roleId as string))"
>
<button type="button" class="team-batch__chip-role" :class="{ 'is-override': roleOverrides.has(uid) }">
{{
roleOverrides.get(uid)
? getRoleNameById(roleOverrides.get(uid)!)
: defaultRoleId
? getRoleNameById(defaultRoleId)
: '默认'
}}
</button>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="__default__">使用默认</ElDropdownItem>
<ElDropdownItem v-for="role in selectableRoles" :key="role.id" :command="role.id">
<div class="team-batch__role-option">
<span class="team-batch__role-option-name">{{ role.name }}</span>
<ElTooltip v-if="role.remark" :content="role.remark" placement="right" :show-after="120">
<icon-ep:info-filled class="team-batch__role-option-info" @click.stop />
</ElTooltip>
</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<button type="button" class="team-batch__chip-x" @click="toggleUser(uid)">×</button>
</span>
<ElPopover
v-if="overflowSelectedCount > 0"
:visible="overflowPopoverVisible"
placement="top-end"
:width="360"
popper-class="team-batch__overflow-popper"
>
<template #reference>
<button
ref="overflowReferenceEl"
type="button"
class="team-batch__chip-more"
@click="overflowPopoverVisible = !overflowPopoverVisible"
>
+{{ overflowSelectedCount }} 更多
</button>
</template>
<div class="team-batch__overflow-head">
<span>
另外
<strong>{{ overflowSelectedCount }}</strong>
</span>
<button type="button" class="team-batch__link team-batch__link--danger" @click="clearAll">
清空全部
</button>
</div>
<div class="team-batch__overflow-chips">
<span v-for="uid in [...selected].slice(4)" :key="uid" class="team-batch__chip">
<span class="team-batch__chip-name">{{ getUserById(uid)?.nickname }}</span>
<ElDropdown
trigger="click"
@command="roleId => setRoleOverride(uid, roleId === '__default__' ? null : (roleId as string))"
>
<button
type="button"
class="team-batch__chip-role"
:class="{ 'is-override': roleOverrides.has(uid) }"
>
{{
roleOverrides.get(uid)
? getRoleNameById(roleOverrides.get(uid)!)
: defaultRoleId
? getRoleNameById(defaultRoleId)
: '默认'
}}
</button>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="__default__">使用默认</ElDropdownItem>
<ElDropdownItem v-for="role in selectableRoles" :key="role.id" :command="role.id">
<div class="team-batch__role-option">
<span class="team-batch__role-option-name">{{ role.name }}</span>
<ElTooltip v-if="role.remark" :content="role.remark" placement="right" :show-after="120">
<icon-ep:info-filled class="team-batch__role-option-info" @click.stop />
</ElTooltip>
</div>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<button type="button" class="team-batch__chip-x" @click="toggleUser(uid)">×</button>
</span>
</div>
</ElPopover>
</div>
</div>
<div class="team-batch__params">
<div class="team-batch__params-grid">
<div class="team-batch__field">
<label class="team-batch__field-label">
默认角色
<span class="team-batch__required">*</span>
</label>
<ElSelect v-model="defaultRoleId" placeholder="请选择默认角色">
<ElOption v-for="role in selectableRoles" :key="role.id" :label="role.name" :value="role.id">
<div class="team-batch__role-option">
<span class="team-batch__role-option-name">{{ role.name }}</span>
<ElTooltip v-if="role.remark" :content="role.remark" placement="right" :show-after="120">
<icon-ep:info-filled class="team-batch__role-option-info" @click.stop />
</ElTooltip>
</div>
</ElOption>
</ElSelect>
</div>
<div class="team-batch__field">
<label class="team-batch__field-label">备注(可选,统一应用)</label>
<ElInput v-model="batchRemark" maxlength="200" placeholder="如:本次批量加入" />
</div>
</div>
</div>
</div>
</BusinessFormDialog>
</template>
<style scoped>
.team-batch {
display: flex;
flex-direction: column;
gap: 10px;
}
.team-batch__tabs {
display: flex;
gap: 2px;
border-bottom: 1px solid var(--el-border-color);
}
.team-batch__tab {
padding: 6px 14px;
border: none;
background: transparent;
cursor: pointer;
font-size: 12.5px;
color: var(--el-text-color-regular);
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.team-batch__tab.is-active {
color: var(--el-color-primary);
border-bottom-color: var(--el-color-primary);
font-weight: 600;
}
.team-batch__picker {
display: grid;
grid-template-columns: 240px 1fr;
gap: 12px;
height: min(260px, 40vh);
min-height: 240px;
}
.team-batch__col {
display: flex;
flex-direction: column;
border: 1px solid var(--el-border-color);
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.team-batch__col-head {
padding: 6px 10px;
border-bottom: 1px solid var(--el-border-color);
background: #fafbfc;
font-size: 12px;
color: var(--el-text-color-regular);
}
.team-batch__col-body {
flex: 1;
overflow-y: auto;
}
.team-batch__placeholder {
padding: 24px;
color: var(--el-text-color-placeholder);
font-size: 12px;
text-align: center;
}
.team-batch__params {
border: 1px solid var(--el-border-color);
border-radius: 6px;
background: #fff;
}
.team-batch__tree {
padding: 4px;
background: transparent;
}
.team-batch__tree :deep(.el-tree-node__content) {
height: 32px;
padding-right: 8px !important;
border-radius: 4px;
transition: background-color 0.15s ease;
}
.team-batch__tree :deep(.el-tree-node__content:hover) {
background: var(--el-fill-color-light);
}
.team-batch__tree :deep(.el-tree-node__expand-icon) {
padding: 4px;
color: var(--el-text-color-placeholder);
font-size: 14px;
}
.team-batch__tree :deep(.el-tree-node__expand-icon.is-leaf) {
color: transparent;
}
.team-batch__node {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
height: 100%;
font-size: 13px;
color: var(--el-text-color-primary);
}
.team-batch__node.is-active {
color: var(--el-color-primary);
font-weight: 500;
}
.team-batch__node-check {
position: relative;
flex-shrink: 0;
width: 14px;
height: 14px;
border: 1px solid var(--el-border-color);
border-radius: 2px;
background: var(--el-bg-color);
cursor: pointer;
transition:
border-color 0.15s ease,
background-color 0.15s ease;
}
.team-batch__node-check:hover {
border-color: var(--el-color-primary);
}
.team-batch__node-check.is-checked {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
}
.team-batch__node-check.is-checked::after {
content: '';
position: absolute;
top: 1px;
left: 4px;
width: 3px;
height: 7px;
border: solid #fff;
border-width: 0 1px 1px 0;
transform: rotate(45deg);
}
.team-batch__node-check.is-partial {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
}
.team-batch__node-check.is-partial::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 2px;
margin: -1px 0 0 -4px;
background: #fff;
border-radius: 1px;
}
.team-batch__node-icon {
flex-shrink: 0;
font-size: 15px;
color: var(--el-text-color-secondary);
transition: color 0.15s ease;
}
.team-batch__node.is-active .team-batch__node-icon {
color: var(--el-color-primary);
}
.team-batch__node-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.team-batch__node-meta {
flex-shrink: 0;
padding-left: 6px;
font-size: 12px;
color: var(--el-text-color-placeholder);
font-variant-numeric: tabular-nums;
}
.team-batch__node.is-active .team-batch__node-meta {
color: var(--el-color-primary);
opacity: 0.7;
}
.team-batch__user-row {
display: flex;
align-items: center;
gap: 10px;
padding: 0 10px;
height: 36px;
border-bottom: 1px solid var(--el-border-color-lighter);
cursor: pointer;
}
.team-batch__user-row:hover {
background: var(--el-fill-color);
}
.team-batch__user-row.is-disabled {
opacity: 0.55;
cursor: not-allowed;
}
.team-batch__user-row.is-disabled:hover {
background: transparent;
}
.team-batch__user-avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(135deg, #c7d2fe, #93c5fd);
color: #fff;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.team-batch__user-main {
flex: 1;
min-width: 0;
overflow: hidden;
}
.team-batch__user-name {
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.team-batch__user-tag {
flex-shrink: 0;
padding: 1px 7px;
border-radius: 999px;
font-size: 11px;
background: var(--el-color-warning-light-7);
color: var(--el-color-warning-dark-2);
}
.team-batch__empty {
padding: 40px 0;
text-align: center;
color: var(--el-text-color-placeholder);
font-size: 12px;
}
.team-batch__selected {
padding: 8px 12px;
background: #f8fafc;
border: 1px solid var(--el-border-color);
border-radius: 6px;
}
.team-batch__selected-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
font-size: 11.5px;
color: var(--el-text-color-regular);
}
.team-batch__selected-head strong {
color: var(--el-color-primary);
font-weight: 700;
font-size: 12.5px;
}
.team-batch__selected-empty {
display: flex;
align-items: center;
min-height: 26px;
color: var(--el-text-color-placeholder);
font-size: 11.5px;
}
.team-batch__chips {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-height: 26px;
}
.team-batch__chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 4px 2px 8px;
background: #fff;
border: 1px solid var(--el-border-color-darker);
border-radius: 999px;
font-size: 11.5px;
}
.team-batch__chip-x {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--el-fill-color);
color: var(--el-text-color-regular);
border: none;
cursor: pointer;
font-size: 11px;
}
.team-batch__chip-x:hover {
background: var(--el-color-danger);
color: #fff;
}
.team-batch__chip-more {
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 10px;
border-radius: 999px;
border: 1px dashed var(--el-border-color-darker);
background: transparent;
color: var(--el-color-primary);
font-size: 11.5px;
cursor: pointer;
transition:
border-color 0.15s ease,
background-color 0.15s ease;
}
.team-batch__chip-more:hover {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.team-batch__overflow-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 12px;
color: var(--el-text-color-regular);
}
.team-batch__overflow-head strong {
color: var(--el-color-primary);
font-weight: 700;
}
.team-batch__overflow-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
max-height: 260px;
overflow-y: auto;
}
.team-batch__link {
background: transparent;
border: none;
cursor: pointer;
font-size: 11.5px;
padding: 0;
}
.team-batch__link--danger {
color: var(--el-color-danger);
}
.team-batch__link:hover {
text-decoration: underline;
}
.team-batch__params {
padding: 10px 12px;
}
.team-batch__params-grid {
display: grid;
grid-template-columns: 240px 1fr;
gap: 12px;
}
.team-batch__field {
display: flex;
flex-direction: column;
gap: 4px;
}
.team-batch__field-label {
font-size: 11.5px;
color: var(--el-text-color-primary);
font-weight: 500;
}
.team-batch__required {
color: var(--el-color-danger);
}
.team-batch__picker.is-single {
grid-template-columns: 1fr;
}
.team-batch__chip-role {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 0 6px;
height: 18px;
border-radius: 999px;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-size: 11px;
cursor: pointer;
border: none;
}
.team-batch__chip-role.is-override {
background: var(--el-color-warning-light-7);
color: var(--el-color-warning-dark-2);
}
.team-batch__override-cnt {
color: var(--el-color-warning-dark-2);
font-weight: 700;
}
.team-batch__search {
padding: 6px 10px;
border-bottom: 1px solid var(--el-border-color);
}
.team-batch__col-head--user {
display: flex;
justify-content: space-between;
align-items: center;
}
.team-batch__hide-added {
font-size: 11.5px;
}
.team-batch__empty-action {
display: block;
margin: 6px auto 0;
}
.team-batch__role-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
}
.team-batch__role-option-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.team-batch__role-option-info {
flex-shrink: 0;
font-size: 14px;
color: var(--el-text-color-placeholder);
cursor: help;
}
.team-batch__role-option-info:hover {
color: var(--el-color-primary);
}
</style>