- 布局对齐「分配应用用户」:左「全部人员」(本校教职工候选)+ 中间 < > 箭头 + 右「已有成员」 (原先左侧是当前成员、右侧是候选勾选,方向与本系统其他页面相反) - 两栏都带搜索框、全选框与计数;左栏保留服务端 keyword 检索(候选上限 200) - 交互改为待提交模式:箭头只改本地 pending,点「保存」才先 Add 再 Remove, 取消整体丢弃;未产生变更时保存按钮禁用,底部显示「新增 N 人 · 移出 M 人」 - 支持双击行快速移动;移出后的人员回到左侧列表,方便反悔 - 全选/半选按「当前可见行」判断,关键字筛选后不会误判
This commit is contained in:
@@ -1,41 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<Modal v-model="innerVisible" :title="title" width="720" :mask-closable="false"
|
<Modal v-model="innerVisible" :title="title" width="760" :mask-closable="false"
|
||||||
@on-visible-change="onVisibleChange">
|
@on-visible-change="onVisibleChange">
|
||||||
<div class="member-body">
|
<div class="member-body">
|
||||||
<!-- 已有成员 -->
|
<!-- 左:全部人员(本校教职工,服务端已排除学生组与在册成员) -->
|
||||||
<div class="pane">
|
<section class="pane">
|
||||||
<div class="pane-head">
|
<div class="pane-head">
|
||||||
<span class="pane-title">当前成员</span>
|
<Checkbox :model-value="leftAllChecked" :indeterminate="leftIndeterminate"
|
||||||
<span class="pane-sub">{{ members.length }} 人</span>
|
@on-change="(v: any) => toggleAll(leftRows, leftChecked, !!v)" />
|
||||||
|
<span class="pane-title">全部人员</span>
|
||||||
|
<span class="pane-sub">{{ leftCheckedCount }}/{{ leftRows.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loadingMembers" class="pane-tip">加载中…</div>
|
<div class="pane-filter">
|
||||||
<div v-else-if="!members.length" class="pane-tip">
|
<Input v-model="kwLeft" size="small" placeholder="请输入搜索内容" clearable search
|
||||||
该部门还没有成员。成员只影响「派人」,不影响部门本身的启停与展示。
|
@on-search="loadCandidates" @on-change="onKwLeftChange" />
|
||||||
</div>
|
|
||||||
<ul v-else class="pane-list">
|
|
||||||
<li v-for="u in members" :key="u.userId" class="row">
|
|
||||||
<span class="row-name">{{ u.name || u.account }}</span>
|
|
||||||
<span class="row-sub">{{ u.account }}</span>
|
|
||||||
<a class="row-op danger" @click="remove(u)">移除</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 候选教职工 -->
|
|
||||||
<div class="pane">
|
|
||||||
<div class="pane-head">
|
|
||||||
<Input v-model="kw" placeholder="按账号/姓名搜索" clearable search enter-button
|
|
||||||
@on-search="loadCandidates" @on-change="onKwChange" class="kw" />
|
|
||||||
<span class="pane-sub">已选 {{ selectedIds.size }} 人</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loadingCandidates" class="pane-tip">加载中…</div>
|
<div v-if="loadingCandidates" class="pane-tip">加载中…</div>
|
||||||
<div v-else-if="!candidates.length" class="pane-tip">
|
<div v-else-if="!leftRows.length" class="pane-tip">
|
||||||
没有可添加的教职工。候选=本校教职工(系统会自动排除学生组下的人员),且尚未在该部门。
|
{{ kwLeft ? '没有匹配的人员' : '没有可添加的教职工' }}
|
||||||
</div>
|
</div>
|
||||||
<ul v-else class="pane-list">
|
<ul v-else class="pane-list">
|
||||||
<li v-for="u in candidates" :key="u.userId" class="row">
|
<li v-for="u in leftRows" :key="u.userId" class="row" @dblclick="moveRightOne(u)">
|
||||||
<Checkbox :model-value="selectedIds.has(u.userId as number)"
|
<Checkbox :model-value="isChecked(leftChecked, u)" @on-change="(v: any) => toggleOne(leftChecked, u, !!v)">
|
||||||
@on-change="(v: any) => toggle(u.userId as number, !!v)">
|
|
||||||
<span class="row-name">{{ u.name || u.account }}</span>
|
<span class="row-name">{{ u.name || u.account }}</span>
|
||||||
<span class="row-sub">{{ u.account }}</span>
|
<span class="row-sub">{{ u.account }}</span>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -44,13 +29,48 @@
|
|||||||
<div v-if="candidates.length >= CANDIDATE_PAGE_HINT" class="pane-foot">
|
<div v-if="candidates.length >= CANDIDATE_PAGE_HINT" class="pane-foot">
|
||||||
仅显示前 {{ CANDIDATE_PAGE_HINT }} 条,可输入关键字缩小范围。
|
仅显示前 {{ CANDIDATE_PAGE_HINT }} 条,可输入关键字缩小范围。
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 中:加入 / 移出 -->
|
||||||
|
<div class="mid">
|
||||||
|
<Button size="small" class="mid-btn" :disabled="!rightCheckedCount" title="移出成员"
|
||||||
|
@click="moveLeft()"><</Button>
|
||||||
|
<Button size="small" type="primary" class="mid-btn" :disabled="!leftCheckedCount"
|
||||||
|
title="加入成员" @click="moveRight()">></Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 右:已有成员 -->
|
||||||
|
<section class="pane">
|
||||||
|
<div class="pane-head">
|
||||||
|
<Checkbox :model-value="rightAllChecked" :indeterminate="rightIndeterminate"
|
||||||
|
@on-change="(v: any) => toggleAll(rightRows, rightChecked, !!v)" />
|
||||||
|
<span class="pane-title">已有成员</span>
|
||||||
|
<span class="pane-sub">{{ rightRows.length }} 人</span>
|
||||||
|
</div>
|
||||||
|
<div class="pane-filter">
|
||||||
|
<Input v-model="kwRight" size="small" placeholder="请输入搜索内容" clearable />
|
||||||
|
</div>
|
||||||
|
<div v-if="loadingMembers" class="pane-tip">加载中…</div>
|
||||||
|
<div v-else-if="!rightRows.length" class="pane-tip">
|
||||||
|
{{ kwRight ? '没有匹配的成员' : '该部门还没有成员' }}
|
||||||
|
</div>
|
||||||
|
<ul v-else class="pane-list">
|
||||||
|
<li v-for="u in rightRows" :key="u.userId" class="row" @dblclick="moveLeftOne(u)">
|
||||||
|
<Checkbox :model-value="isChecked(rightChecked, u)" @on-change="(v: any) => toggleOne(rightChecked, u, !!v)">
|
||||||
|
<span class="row-name">{{ u.name || u.account }}</span>
|
||||||
|
<span class="row-sub">{{ u.account }}</span>
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button @click="innerVisible = false">关闭</Button>
|
<div class="modal-footer">
|
||||||
<Button type="primary" :loading="saving" :disabled="!selectedIds.size" @click="addSelected">
|
<span v-if="pendingText" class="pending-tip">{{ pendingText }}</span>
|
||||||
添加所选
|
<Button @click="innerVisible = false">取消</Button>
|
||||||
</Button>
|
<Button type="primary" :loading="saving" :disabled="!hasPending" @click="save">保存</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -62,9 +82,10 @@ import Apis from "@/api";
|
|||||||
import type { CustomDepartmentMemberVo, CustomDepartmentVo } from "@/api/globals";
|
import type { CustomDepartmentMemberVo, CustomDepartmentVo } from "@/api/globals";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义部门成员管理弹窗。
|
* 自定义部门成员管理弹窗(穿梭框式)。
|
||||||
* 候选走 {@code /api/customdept/CandidateUsers/{id}}:服务端按「本校教职工 + 排除学生组 + 未在本部门」给列表,
|
* 左「全部人员」=本校教职工候选(服务端按「排除学生组 + 未在本部门」给),右「已有成员」=部门在册成员。
|
||||||
* 所以这里不需要(也不能)用组维度的 GroupUsers 当候选源——那会把学生带进来(任务书 D4)。
|
* 中间箭头只改本地待提交状态,点「保存」才一次性提交:先 Add 再 Remove,取消则整体丢弃。
|
||||||
|
* 候选走 {@code /api/customdept/CandidateUsers/{id}},不能用组维度的 GroupUsers(会带进学生)。
|
||||||
*/
|
*/
|
||||||
const props = defineProps<{ visible: boolean; dept?: CustomDepartmentVo | null }>();
|
const props = defineProps<{ visible: boolean; dept?: CustomDepartmentVo | null }>();
|
||||||
const emit = defineEmits<{ "update:visible": [boolean]; success: [] }>();
|
const emit = defineEmits<{ "update:visible": [boolean]; success: [] }>();
|
||||||
@@ -78,25 +99,153 @@ const innerVisible = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const title = computed(() => `部门成员 · ${props.dept?.name || ""}`);
|
const title = computed(() => `部门成员 · ${props.dept?.name || ""}`);
|
||||||
|
const deptId = computed(() => props.dept?.id ?? null);
|
||||||
|
|
||||||
|
/** 服务端数据 */
|
||||||
const members = ref<CustomDepartmentMemberVo[]>([]);
|
const members = ref<CustomDepartmentMemberVo[]>([]);
|
||||||
const candidates = ref<CustomDepartmentMemberVo[]>([]);
|
const candidates = ref<CustomDepartmentMemberVo[]>([]);
|
||||||
const selectedIds = ref<Set<number>>(new Set());
|
|
||||||
const kw = ref("");
|
|
||||||
const loadingMembers = ref(false);
|
const loadingMembers = ref(false);
|
||||||
const loadingCandidates = ref(false);
|
const loadingCandidates = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
|
|
||||||
const deptId = computed(() => props.dept?.id ?? null);
|
/** 待提交:新增(左→右)与移出(右→左)。点保存才落库,取消整体丢弃 */
|
||||||
|
const pendingAdd = ref<CustomDepartmentMemberVo[]>([]);
|
||||||
|
const pendingRemove = ref<Set<number>>(new Set());
|
||||||
|
|
||||||
|
const kwLeft = ref("");
|
||||||
|
const kwRight = ref("");
|
||||||
|
const leftChecked = ref<Set<number>>(new Set());
|
||||||
|
const rightChecked = ref<Set<number>>(new Set());
|
||||||
|
|
||||||
|
function matchKw(u: CustomDepartmentMemberVo, k: string) {
|
||||||
|
return (
|
||||||
|
String(u.name || "").toLowerCase().includes(k) ||
|
||||||
|
String(u.account || "").toLowerCase().includes(k)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 左列表:候选池去掉待加入的,再补上「待移出的成员」——
|
||||||
|
* 服务端候选池天然不含在册成员,若不补回来,移出后想反悔就找不到了。
|
||||||
|
*/
|
||||||
|
const leftRows = computed<CustomDepartmentMemberVo[]>(() => {
|
||||||
|
const k = kwLeft.value.trim().toLowerCase();
|
||||||
|
const hit = (u: CustomDepartmentMemberVo) => !k || matchKw(u, k);
|
||||||
|
const back = members.value.filter((m) => pendingRemove.value.has(m.userId as number) && hit(m));
|
||||||
|
const pool = candidates.value.filter(
|
||||||
|
(c) => !pendingRemove.value.has(c.userId as number) && !isPendingAdd(c.userId as number) && hit(c)
|
||||||
|
);
|
||||||
|
return [...back, ...pool];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 右列表:在册成员去掉待移出的,加上待加入的 */
|
||||||
|
const rightRows = computed<CustomDepartmentMemberVo[]>(() => {
|
||||||
|
const k = kwRight.value.trim().toLowerCase();
|
||||||
|
const hit = (u: CustomDepartmentMemberVo) => !k || matchKw(u, k);
|
||||||
|
const held = members.value.filter((m) => !pendingRemove.value.has(m.userId as number));
|
||||||
|
return [...held, ...pendingAdd.value].filter(hit);
|
||||||
|
});
|
||||||
|
|
||||||
|
function isPendingAdd(userId: number) {
|
||||||
|
return pendingAdd.value.some((u) => u.userId === userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPending = computed(() => pendingAdd.value.length > 0 || pendingRemove.value.size > 0);
|
||||||
|
const pendingText = computed(() => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (pendingAdd.value.length) parts.push(`新增 ${pendingAdd.value.length} 人`);
|
||||||
|
if (pendingRemove.value.size) parts.push(`移出 ${pendingRemove.value.size} 人`);
|
||||||
|
return parts.join(" · ");
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ================= 勾选 ================= */
|
||||||
|
|
||||||
|
function isChecked(box: Set<number>, u: CustomDepartmentMemberVo) {
|
||||||
|
return box.has(u.userId as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleOne(box: Set<number>, u: CustomDepartmentMemberVo, checked: boolean) {
|
||||||
|
const userId = u.userId as number;
|
||||||
|
if (checked) box.add(userId);
|
||||||
|
else box.delete(userId);
|
||||||
|
// Set 自身不是响应式依赖,换一个实例触发重算
|
||||||
|
if (box === leftChecked.value) leftChecked.value = new Set(box);
|
||||||
|
else rightChecked.value = new Set(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRightOne(u: CustomDepartmentMemberVo) {
|
||||||
|
if (u.userId) moveRight([u.userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveLeftOne(u: CustomDepartmentMemberVo) {
|
||||||
|
if (u.userId) moveLeft([u.userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAll(rows: CustomDepartmentMemberVo[], box: Set<number>, checked: boolean) {
|
||||||
|
const ids = rows.map((u) => u.userId as number);
|
||||||
|
const next = new Set(box);
|
||||||
|
ids.forEach((id) => (checked ? next.add(id) : next.delete(id)));
|
||||||
|
if (box === leftChecked.value) leftChecked.value = next;
|
||||||
|
else rightChecked.value = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全选 / 半选一律按「当前可见行」判断:关键字筛选后仍被勾着的行不算数 */
|
||||||
|
const leftCheckedCount = computed(
|
||||||
|
() => leftRows.value.filter((u) => leftChecked.value.has(u.userId as number)).length
|
||||||
|
);
|
||||||
|
const rightCheckedCount = computed(
|
||||||
|
() => rightRows.value.filter((u) => rightChecked.value.has(u.userId as number)).length
|
||||||
|
);
|
||||||
|
const leftAllChecked = computed(() => leftRows.value.length > 0 && leftCheckedCount.value >= leftRows.value.length);
|
||||||
|
const leftIndeterminate = computed(() => leftCheckedCount.value > 0 && !leftAllChecked.value);
|
||||||
|
const rightAllChecked = computed(() => rightRows.value.length > 0 && rightCheckedCount.value >= rightRows.value.length);
|
||||||
|
const rightIndeterminate = computed(() => rightCheckedCount.value > 0 && !rightAllChecked.value);
|
||||||
|
|
||||||
|
/* ================= 左右移动 ================= */
|
||||||
|
|
||||||
|
/** 加入:左侧勾选(或双击某行)→ 挂到待新增 */
|
||||||
|
function moveRight(userIds?: number[]) {
|
||||||
|
const ids = userIds ?? leftRows.value.filter((u) => leftChecked.value.has(u.userId as number)).map((u) => u.userId as number);
|
||||||
|
if (!ids.length) return;
|
||||||
|
const picked = leftRows.value.filter((u) => ids.includes(u.userId as number));
|
||||||
|
pendingAdd.value = [...pendingAdd.value, ...picked];
|
||||||
|
// 移出后再加入 = 撤销移出,不用重复 Add
|
||||||
|
const nextRemove = new Set(pendingRemove.value);
|
||||||
|
picked.forEach((u) => nextRemove.delete(u.userId as number));
|
||||||
|
pendingRemove.value = nextRemove;
|
||||||
|
leftChecked.value = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移出:右侧勾选(或双击某行)→ 撤掉待新增,或记账到待移出 */
|
||||||
|
function moveLeft(userIds?: number[]) {
|
||||||
|
const ids = userIds ?? rightRows.value.filter((u) => rightChecked.value.has(u.userId as number)).map((u) => u.userId as number);
|
||||||
|
if (!ids.length) return;
|
||||||
|
const stillPending = new Set<number>();
|
||||||
|
pendingAdd.value = pendingAdd.value.filter((u) => {
|
||||||
|
if (ids.includes(u.userId as number)) {
|
||||||
|
stillPending.add(u.userId as number);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
const nextRemove = new Set(pendingRemove.value);
|
||||||
|
ids.forEach((id) => {
|
||||||
|
if (!stillPending.has(id)) nextRemove.add(id);
|
||||||
|
});
|
||||||
|
pendingRemove.value = nextRemove;
|
||||||
|
rightChecked.value = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= 数据加载 ================= */
|
||||||
|
|
||||||
async function loadMembers() {
|
async function loadMembers() {
|
||||||
if (!deptId.value) return;
|
if (!deptId.value) return;
|
||||||
loadingMembers.value = true;
|
loadingMembers.value = true;
|
||||||
try {
|
try {
|
||||||
members.value = (await Apis.CustomDept.customDeptMembers({
|
members.value = ((await Apis.CustomDept.customDeptMembers({
|
||||||
pathParams: { id: deptId.value },
|
pathParams: { id: deptId.value },
|
||||||
cacheFor: null,
|
cacheFor: null,
|
||||||
})) as CustomDepartmentMemberVo[] || [];
|
})) as CustomDepartmentMemberVo[]) || [];
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.warn(e?.message || "加载部门成员失败");
|
console.warn(e?.message || "加载部门成员失败");
|
||||||
members.value = [];
|
members.value = [];
|
||||||
@@ -109,11 +258,11 @@ async function loadCandidates() {
|
|||||||
if (!deptId.value) return;
|
if (!deptId.value) return;
|
||||||
loadingCandidates.value = true;
|
loadingCandidates.value = true;
|
||||||
try {
|
try {
|
||||||
candidates.value = (await Apis.CustomDept.customDeptCandidateUsers({
|
candidates.value = ((await Apis.CustomDept.customDeptCandidateUsers({
|
||||||
pathParams: { id: deptId.value },
|
pathParams: { id: deptId.value },
|
||||||
params: { keyword: kw.value.trim() || undefined },
|
params: { keyword: kwLeft.value.trim() || undefined },
|
||||||
cacheFor: null,
|
cacheFor: null,
|
||||||
})) as CustomDepartmentMemberVo[] || [];
|
})) as CustomDepartmentMemberVo[]) || [];
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.warn(e?.message || "加载候选教职工失败");
|
console.warn(e?.message || "加载候选教职工失败");
|
||||||
candidates.value = [];
|
candidates.value = [];
|
||||||
@@ -122,58 +271,56 @@ async function loadCandidates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKwChange() {
|
function onKwLeftChange() {
|
||||||
// clearable 清空时也当一次搜索
|
// clearable 清空时也当一次搜索
|
||||||
if (!kw.value) loadCandidates();
|
if (!kwLeft.value) loadCandidates();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggle(userId: number, checked: boolean) {
|
/* ================= 保存 ================= */
|
||||||
if (checked) selectedIds.value.add(userId);
|
|
||||||
else selectedIds.value.delete(userId);
|
|
||||||
selectedIds.value = new Set(selectedIds.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addSelected() {
|
async function save() {
|
||||||
if (!deptId.value || !selectedIds.value.size) return;
|
if (!deptId.value || !hasPending.value) return;
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
try {
|
try {
|
||||||
await Apis.CustomDept.customDeptAddMembers({
|
if (pendingAdd.value.length) {
|
||||||
pathParams: { id: deptId.value },
|
await Apis.CustomDept.customDeptAddMembers({
|
||||||
data: { schoolId: props.dept?.schoolId as number, userIds: [...selectedIds.value] },
|
pathParams: { id: deptId.value },
|
||||||
cacheFor: null,
|
data: {
|
||||||
});
|
schoolId: props.dept?.schoolId as number,
|
||||||
Message.success("已添加");
|
userIds: pendingAdd.value.map((u) => u.userId as number),
|
||||||
selectedIds.value = new Set();
|
},
|
||||||
await Promise.all([loadMembers(), loadCandidates()]);
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (pendingRemove.value.size) {
|
||||||
|
await Apis.CustomDept.customDeptRemoveMembers({
|
||||||
|
pathParams: { id: deptId.value },
|
||||||
|
data: { schoolId: props.dept?.schoolId as number, userIds: [...pendingRemove.value] },
|
||||||
|
cacheFor: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Message.success("已保存");
|
||||||
emit("success");
|
emit("success");
|
||||||
|
innerVisible.value = false;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.warn(e?.message || "添加成员失败");
|
// 失败提示由全局 alova 拦截器弹出,弹窗保持打开让用户重试
|
||||||
|
console.warn(e?.message || "保存成员失败");
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false;
|
saving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(u: CustomDepartmentMemberVo) {
|
/* ================= 生命周期 ================= */
|
||||||
if (!deptId.value || !u.userId) return;
|
|
||||||
try {
|
|
||||||
await Apis.CustomDept.customDeptRemoveMembers({
|
|
||||||
pathParams: { id: deptId.value },
|
|
||||||
data: { schoolId: props.dept?.schoolId as number, userIds: [u.userId] },
|
|
||||||
cacheFor: null,
|
|
||||||
});
|
|
||||||
Message.success("已移除");
|
|
||||||
await Promise.all([loadMembers(), loadCandidates()]);
|
|
||||||
emit("success");
|
|
||||||
} catch (e: any) {
|
|
||||||
console.warn(e?.message || "移除成员失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
members.value = [];
|
members.value = [];
|
||||||
candidates.value = [];
|
candidates.value = [];
|
||||||
selectedIds.value = new Set();
|
pendingAdd.value = [];
|
||||||
kw.value = "";
|
pendingRemove.value = new Set();
|
||||||
|
leftChecked.value = new Set();
|
||||||
|
rightChecked.value = new Set();
|
||||||
|
kwLeft.value = "";
|
||||||
|
kwRight.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function onVisibleChange(v: boolean) {
|
function onVisibleChange(v: boolean) {
|
||||||
@@ -192,8 +339,9 @@ watch(() => props.visible, (v) => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.member-body {
|
.member-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
align-items: stretch;
|
||||||
height: 440px;
|
gap: 8px;
|
||||||
|
height: 430px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane {
|
.pane {
|
||||||
@@ -201,14 +349,20 @@ watch(() => props.visible, (v) => {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--line, #e2e6ef);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-head {
|
.pane-head {
|
||||||
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
height: 38px;
|
||||||
min-height: 30px;
|
padding: 0 10px;
|
||||||
|
border-bottom: 1px solid #f1f4f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-title {
|
.pane-title {
|
||||||
@@ -224,19 +378,18 @@ watch(() => props.visible, (v) => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kw {
|
.pane-filter {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
|
padding: 8px 10px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-list {
|
.pane-list {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 6px 10px;
|
padding: 0 6px 6px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid var(--line, #e9edf5);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-tip {
|
.pane-tip {
|
||||||
@@ -245,46 +398,88 @@ watch(() => props.visible, (v) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 12px;
|
padding: 0 16px;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
|
line-height: 1.8;
|
||||||
color: var(--text-3, #98a1b2);
|
color: var(--text-3, #98a1b2);
|
||||||
border: 1px dashed var(--line, #e9edf5);
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fbfcfd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pane-foot {
|
.pane-foot {
|
||||||
padding-top: 6px;
|
flex: 0 0 auto;
|
||||||
|
padding: 6px 10px 8px;
|
||||||
font-size: 11.5px;
|
font-size: 11.5px;
|
||||||
color: var(--text-3, #98a1b2);
|
color: var(--text-3, #98a1b2);
|
||||||
|
border-top: 1px solid #f6f8fb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
height: 30px;
|
||||||
padding: 7px 4px;
|
padding: 0 4px;
|
||||||
border-bottom: 1px solid #f5f5f5;
|
border-radius: 3px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ivu-checkbox-wrapper) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ivu-checkbox) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-name {
|
.row-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #272829;
|
color: #272829;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-sub {
|
.row-sub {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-left: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #9aa1ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-op {
|
/* 中间箭头列 */
|
||||||
margin-left: auto;
|
.mid {
|
||||||
font-size: 12px;
|
flex: 0 0 36px;
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-op.danger {
|
.mid-btn {
|
||||||
color: #e54545;
|
width: 32px;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-tip {
|
||||||
|
margin-right: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-3, #98a1b2);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user