- 修 bug:DeptRow 的 children 改名为 _kids —— iView Table 见行上有 children 会切树形数据模式, 与自画的缩进/折叠打架,表现为开启「包含子组」时整表空白(关闭时 children 为空数组才恢复正常) - 关闭「包含子组」时仍保留「N 个下级」灰标,只收起子行,层级信息不再丢 - 状态列从边框 Tag 改回胶囊开关(与「包含子组」同一套样式) - 人员列:最多平铺 10 个姓名 + N,全部列完(≤10 人)时不可点击; 姓名区可截断、+N 固定不收缩;列宽改为 序号70 / 名称236 / 上级130 / 人员自适应 / 状态90 - 自定义部门左栏改轻量分类列表(无边框卡片、选中浅底 + 左侧细蓝条), 分类的排序 / 改名 / 删除 / 新增收进「分类管理」弹框:行内改名(回车或失焦保存,Esc 取消)、 有部门时删除禁用、底部新增分类;保存排序沿用 customDeptCategorySort - 自定义部门表格同步:状态胶囊开关、人员前 10 规则、列宽与系统部门一致
This commit is contained in:
@@ -6,34 +6,21 @@
|
||||
|
||||
<template v-else>
|
||||
<div class="custom-body">
|
||||
<!-- 左侧:分类平铺列表(容器,可拖动排序,↑↓ 保留兜底) -->
|
||||
<!-- 左侧:分类轻量列表(只管选中,排序/改名/删除都在「分类管理」弹框里) -->
|
||||
<div class="cat-col">
|
||||
<div class="col-head">
|
||||
<span class="col-title">自定义分类</span>
|
||||
<Button size="small" type="primary" @click="openCategoryCreate">新增分类</Button>
|
||||
<Button size="small" @click="openCatManage">分类管理</Button>
|
||||
</div>
|
||||
<div v-if="loadingCategories" class="col-tip">加载中…</div>
|
||||
<div v-else-if="!categories.length" class="col-tip">
|
||||
暂无自定义分类。分类只是管理容器,不能启停也不能配成员,请先新建一个。
|
||||
暂无自定义分类。点右上「分类管理」新建一个,分类只是管理容器,不能启停也不能配成员。
|
||||
</div>
|
||||
<ul v-else class="cat-list">
|
||||
<li v-for="(c, idx) in categories" :key="c.id" class="cat-item"
|
||||
:class="{ 'is-active': c.id === activeCategoryId, dragging: catDragIdx === idx }"
|
||||
draggable="true" @dragstart="onCatDragStart(idx)" @dragover.prevent="onCatDragOver(idx)"
|
||||
@drop.prevent="onCatDrop(idx)" @dragend="onCatDragEnd" @click="selectCategory(c.id)">
|
||||
<i class="grip" title="拖动排序">⠿</i>
|
||||
<li v-for="c in categories" :key="c.id" class="cat"
|
||||
:class="{ 'is-active': c.id === activeCategoryId }" @click="selectCategory(c.id)">
|
||||
<span class="cat-name" :title="c.name">{{ c.name }}</span>
|
||||
<span class="cat-count">{{ c.departmentCount ?? 0 }}</span>
|
||||
<span class="cat-ops" @click.stop>
|
||||
<i class="op" :class="{ 'is-disabled': idx === 0 }" title="上移"
|
||||
@click="moveCategory(idx, -1)">↑</i>
|
||||
<i class="op" :class="{ 'is-disabled': idx === categories.length - 1 }" title="下移"
|
||||
@click="moveCategory(idx, 1)">↓</i>
|
||||
<i class="op" title="改名" @click="openCategoryRename(c)">✎</i>
|
||||
<i class="op" :class="{ 'is-disabled': hasDepartments(c) }"
|
||||
:title="hasDepartments(c) ? '分类下还有部门,不能删除' : '删除分类'"
|
||||
@click="removeCategory(c)">×</i>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -51,7 +38,7 @@
|
||||
</Button>
|
||||
</div>
|
||||
<Alert v-if="!activeCategoryId" show-icon type="warning" class="state-tip">
|
||||
请先在左侧选择一个分类,或新建一个分类。
|
||||
请先在左侧选择一个分类,或点「分类管理」新建一个分类。
|
||||
</Alert>
|
||||
<div v-else-if="loadingDepartments" class="col-tip">加载中…</div>
|
||||
<Table v-else class="dept-itable" border :columns="deptColumns" :data="departments"
|
||||
@@ -59,15 +46,18 @@
|
||||
<template #members="{ row }">
|
||||
<Tooltip v-if="Number(row.memberCount || 0)" placement="top" max-width="360"
|
||||
:content="memberTip(row)">
|
||||
<span class="mem-cell" @click="openMembers(row)">{{ memberText(row) }}</span>
|
||||
<span class="mem" :class="memberClickable(row) ? 'mem-cell' : 'mem-plain'"
|
||||
@click="onMemberClick(row)">
|
||||
<span class="names">{{ memberHead(row) }}</span>
|
||||
<span v-if="memberRest(row)" class="more">+{{ memberRest(row) }}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span v-else class="dim">无成员</span>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag type="border" :color="row.enabled ? 'success' : 'error'" class="sta-tag"
|
||||
:title="row.enabled ? '点击停用' : '点击启用'" @click="onToggleDept(row)">
|
||||
{{ row.enabled ? '已启用' : '已停用' }}
|
||||
</Tag>
|
||||
<i class="sw-mini" :class="{ 'is-on': row.enabled }"
|
||||
:title="row.enabled ? '点击停用' : '点击启用'"
|
||||
@click="onToggleDept(row)"></i>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<a class="lnk" @click="openDeptEdit(row)">编辑</a>
|
||||
@@ -79,7 +69,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排序弹框:拖动左侧手柄调整顺序,保存后按新顺序落库 -->
|
||||
<!-- 分类管理:拖动排序 + 行内改名 / 删除 / 新增 -->
|
||||
<Modal v-model="catManage.visible" title="分类管理" width="560" :mask-closable="false">
|
||||
<Alert type="info" show-icon class="sort-tip">
|
||||
拖动左侧手柄调整分类顺序;分类下还有部门时不能删除。
|
||||
</Alert>
|
||||
<ul class="sort-list">
|
||||
<li v-for="(c, idx) in catManage.list" :key="c.id ?? 'new'" class="sort-item"
|
||||
:class="{ dragging: catSortIdx === idx, 'drag-over': catSortOverIdx === idx, editing: catManage.editIdx === idx }"
|
||||
:draggable="catManage.editIdx !== idx" @dragstart="onCatSortStart(idx)"
|
||||
@dragover.prevent="onCatSortOver(idx)" @drop.prevent="onCatSortDrop"
|
||||
@dragend="onCatSortEnd">
|
||||
<i class="grip">⠿</i>
|
||||
<span class="sno">{{ idx + 1 }}</span>
|
||||
<Input v-if="catManage.editIdx === idx" v-model="catManage.editName" size="small"
|
||||
class="snm-input" maxlength="32" @on-enter="commitCatRename"
|
||||
@on-blur="commitCatRename" @on-keydown="onCatKeydown" />
|
||||
<span v-else class="snm" :title="c.name">{{ c.name }}</span>
|
||||
<span class="cCnt">{{ c.departmentCount ?? 0 }} 个部门</span>
|
||||
<span class="row-ops">
|
||||
<a class="lnk" @click.stop="startCatRename(idx)">改名</a>
|
||||
<a class="lnk danger" :class="{ 'is-disabled': hasDepartments(c) }"
|
||||
:title="hasDepartments(c) ? '分类下还有部门,请先移走或删除这些部门' : '删除分类'"
|
||||
@click.stop="removeCategory(c)">删除</a>
|
||||
</span>
|
||||
</li>
|
||||
<li v-if="catManage.adding" class="sort-item editing" draggable="false">
|
||||
<i class="grip">⠿</i>
|
||||
<span class="sno">+</span>
|
||||
<Input v-model="catManage.newName" size="small" class="snm-input" maxlength="32"
|
||||
placeholder="输入分类名称后回车" @on-enter="commitCatAdd" @on-blur="commitCatAdd"
|
||||
@on-keydown="onCatKeydown" />
|
||||
</li>
|
||||
</ul>
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<Button class="footer-left" @click="startCatAdd">+ 新增分类</Button>
|
||||
<Button @click="catManage.visible = false">取消</Button>
|
||||
<Button type="primary" :loading="catManage.saving" @click="saveCatOrder">保存排序</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- 部门排序弹框:拖动左侧手柄调整顺序,保存后按新顺序落库 -->
|
||||
<Modal v-model="sortVisible" :title="`部门排序 · ${activeCategoryName}`" width="520" :mask-closable="false">
|
||||
<Alert type="info" show-icon class="sort-tip">拖动左侧手柄调整顺序,保存后生效。</Alert>
|
||||
<ul class="sort-list">
|
||||
@@ -98,14 +130,14 @@
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- 名称录入(分类新增/改名,部门新增/改名共用) -->
|
||||
<!-- 名称录入(部门新增 / 编辑) -->
|
||||
<Modal v-model="nameModal.visible" :title="nameModal.title" :mask-closable="false" width="420">
|
||||
<Form :label-width="72">
|
||||
<FormItem :label="nameModal.kind === 'dept' ? '部门名称' : '分类名称'">
|
||||
<FormItem label="部门名称">
|
||||
<Input v-model="nameModal.name" :placeholder="nameModal.placeholder"
|
||||
maxlength="64" show-word-limit />
|
||||
</FormItem>
|
||||
<FormItem v-if="nameModal.kind === 'dept' && nameModal.isEdit" label="归属分类">
|
||||
<FormItem v-if="nameModal.isEdit" label="归属分类">
|
||||
<Select v-model="nameModal.categoryId" placeholder="移动到本校其他分类">
|
||||
<Option v-for="c in categories" :key="c.id" :value="c.id">{{ c.name }}</Option>
|
||||
</Select>
|
||||
@@ -124,7 +156,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { computed, nextTick, reactive, ref, watch } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import Apis from "@/api";
|
||||
import type { CustomDeptCategoryVo, CustomDepartmentVo } from "@/api/globals";
|
||||
@@ -135,8 +167,8 @@ import CustomMemberModal from "./CustomMemberModal.vue";
|
||||
* CUSTOM 自定义部门管理面板(部门管理页的第二个来源页签)。
|
||||
* 口径:分类(平铺列表,容器)→ 部门(可启停 / 可配成员 / 软删)→ 成员(本校教职工,服务端排除学生组)。
|
||||
* 学校上下文由父级传入,和 SYSTEM 页签共用,切换来源不改学校。
|
||||
* 2026-10-23:部门改为 iView Table;排序移入「排序管理」弹框拖动(沿用 customDeptSort),
|
||||
* 分类仍是列表内拖动(沿用 customDeptCategorySort)。
|
||||
* 2026-10-26:左栏改轻量列表(只管选中),分类的排序 / 改名 / 删除 / 新增全部收进「分类管理」弹框;
|
||||
* 部门仍是 iView Table,排序走「排序管理」弹框拖动(沿用 customDeptSort)。
|
||||
*/
|
||||
const props = defineProps<{ schoolId?: number | null }>();
|
||||
|
||||
@@ -153,10 +185,10 @@ const activeCategoryName = computed(() => activeCategory.value?.name || "");
|
||||
|
||||
/** iView Table 列定义(排序不在表格里做,行不拖拽) */
|
||||
const deptColumns: any[] = [
|
||||
{ title: "序号", type: "index", width: 80, align: "center" },
|
||||
{ title: "部门名称", key: "name", minWidth: 220 },
|
||||
{ title: "人员", slot: "members", minWidth: 300 },
|
||||
{ title: "状态", slot: "status", width: 120, align: "center" },
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "部门名称", key: "name", width: 236 },
|
||||
{ title: "人员", slot: "members", minWidth: 200 },
|
||||
{ title: "状态", slot: "status", width: 90, align: "center" },
|
||||
{ title: "操作", slot: "actions", width: 160, align: "right" },
|
||||
];
|
||||
function rowClassName(row: any) {
|
||||
@@ -166,7 +198,6 @@ function rowClassName(row: any) {
|
||||
const nameModal = reactive({
|
||||
visible: false,
|
||||
saving: false,
|
||||
kind: "category" as "category" | "dept",
|
||||
isEdit: false,
|
||||
id: null as number | null,
|
||||
name: "",
|
||||
@@ -251,19 +282,33 @@ async function loadMemberNames() {
|
||||
memberNames.value = map;
|
||||
}
|
||||
|
||||
function memberText(d: CustomDepartmentVo) {
|
||||
const names = memberNames.value[d.id as number] || [];
|
||||
const total = Number(d.memberCount || 0);
|
||||
if (!names.length) return `${total} 人`;
|
||||
const head = names.slice(0, 3).join("、");
|
||||
const rest = total - Math.min(3, names.length);
|
||||
return rest > 0 ? `${head} +${rest}` : head;
|
||||
/** 人员列:最多平铺 10 个姓名,超出用「+N」;已全部显示(≤10 人)时不可点 */
|
||||
const MEMBER_PREVIEW = 10;
|
||||
function memberNamesOf(d: CustomDepartmentVo) {
|
||||
return memberNames.value[d.id as number] || [];
|
||||
}
|
||||
function memberHead(d: CustomDepartmentVo) {
|
||||
const names = memberNamesOf(d);
|
||||
// 姓名预览还没回来时先退回人数,避免出现空单元格
|
||||
if (!names.length) return `${Number(d.memberCount || 0)} 人`;
|
||||
return names.slice(0, MEMBER_PREVIEW).join("、");
|
||||
}
|
||||
function memberRest(d: CustomDepartmentVo) {
|
||||
if (!memberNamesOf(d).length) return 0;
|
||||
return Math.max(0, Number(d.memberCount || 0) - Math.min(MEMBER_PREVIEW, memberNamesOf(d).length));
|
||||
}
|
||||
function memberClickable(d: CustomDepartmentVo) {
|
||||
return Number(d.memberCount || 0) > MEMBER_PREVIEW;
|
||||
}
|
||||
function onMemberClick(d: CustomDepartmentVo) {
|
||||
if (memberClickable(d)) openMembers(d);
|
||||
}
|
||||
function memberTip(d: CustomDepartmentVo) {
|
||||
const names = memberNames.value[d.id as number] || [];
|
||||
const names = memberNamesOf(d);
|
||||
const total = Number(d.memberCount || 0);
|
||||
if (!names.length) return `共 ${total} 人 点击查看全部`;
|
||||
const more = total > names.length ? `…(仅预览前 ${names.length} 人)` : "";
|
||||
return `共 ${total} 人:${names.join("、")}${more} 点击查看全部`;
|
||||
return `共 ${total} 人:${names.join("、")}${more}${memberClickable(d) ? " 点击查看全部" : ""}`;
|
||||
}
|
||||
|
||||
function selectCategory(id: number | null) {
|
||||
@@ -280,6 +325,195 @@ watch(() => props.schoolId, () => {
|
||||
|
||||
watch(activeCategoryId, () => loadDepartments());
|
||||
|
||||
/* ================= 分类管理弹框(拖动排序 + 改名 / 删除 / 新增) ================= */
|
||||
|
||||
const catManage = reactive({
|
||||
visible: false,
|
||||
saving: false,
|
||||
list: [] as CustomDeptCategoryVo[],
|
||||
/** 行内改名:>=0 改第 idx 行 */
|
||||
editIdx: null as number | null,
|
||||
editName: "",
|
||||
/** 底部「+ 新增分类」:末尾插一行输入框 */
|
||||
adding: false,
|
||||
newName: "",
|
||||
});
|
||||
/** 改名 / 新增共用一个提交锁:回车会让输入框失焦,失焦又会再触发一次提交 */
|
||||
let catCommitLock = false;
|
||||
|
||||
const catSortIdx = ref<number | null>(null);
|
||||
const catSortOverIdx = ref<number | null>(null);
|
||||
|
||||
function openCatManage() {
|
||||
catManage.list = categories.value.map((c) => ({ ...c }));
|
||||
catManage.editIdx = null;
|
||||
catManage.editName = "";
|
||||
catManage.adding = false;
|
||||
catManage.newName = "";
|
||||
catSortIdx.value = null;
|
||||
catSortOverIdx.value = null;
|
||||
catManage.visible = true;
|
||||
}
|
||||
|
||||
/** 弹框里的增删改后,把服务端最新分类同步进弹框与左栏 */
|
||||
async function reloadCatAll() {
|
||||
await loadCategories();
|
||||
catManage.list = categories.value.map((c) => ({ ...c }));
|
||||
}
|
||||
|
||||
const hasDepartments = (c: CustomDeptCategoryVo) => Number(c.departmentCount || 0) > 0;
|
||||
|
||||
function startCatRename(idx: number) {
|
||||
catManage.adding = false;
|
||||
catManage.editIdx = idx;
|
||||
catManage.editName = catManage.list[idx]?.name || "";
|
||||
nextTick(() => {
|
||||
const el = document.querySelector(".snm-input input") as HTMLInputElement | null;
|
||||
el?.focus();
|
||||
el?.select();
|
||||
});
|
||||
}
|
||||
|
||||
function cancelCatEdit() {
|
||||
catManage.editIdx = null;
|
||||
catManage.editName = "";
|
||||
}
|
||||
|
||||
function onCatKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
cancelCatEdit();
|
||||
catManage.adding = false;
|
||||
catManage.newName = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function commitCatRename() {
|
||||
if (catCommitLock) return;
|
||||
const idx = catManage.editIdx;
|
||||
if (idx === null) return;
|
||||
const row = catManage.list[idx];
|
||||
const name = catManage.editName.trim();
|
||||
if (!row) { cancelCatEdit(); return; }
|
||||
if (!name || name === row.name) { cancelCatEdit(); return; }
|
||||
catCommitLock = true;
|
||||
try {
|
||||
await Apis.CustomDeptCategory.customDeptCategoryEdit({
|
||||
pathParams: { id: row.id as number },
|
||||
data: { schoolId: props.schoolId as number, name },
|
||||
cacheFor: null,
|
||||
});
|
||||
row.name = name;
|
||||
// 左栏同步改名,不必整表重取
|
||||
const hit = categories.value.find((c) => c.id === row.id);
|
||||
if (hit) hit.name = name;
|
||||
Message.success("已保存");
|
||||
} catch (e: any) {
|
||||
// 同名校验等后端拒绝由全局拦截器弹出,输入框保持打开让用户改
|
||||
console.warn(e?.message || "分类改名失败");
|
||||
return;
|
||||
} finally {
|
||||
catCommitLock = false;
|
||||
}
|
||||
// 期间若用户已经点到下一行改名,不要把它刚打开的编辑态关掉
|
||||
if (catManage.editIdx === idx) cancelCatEdit();
|
||||
}
|
||||
|
||||
function startCatAdd() {
|
||||
catManage.editIdx = null;
|
||||
catManage.adding = true;
|
||||
catManage.newName = "";
|
||||
nextTick(() => {
|
||||
const el = document.querySelector(".snm-input input") as HTMLInputElement | null;
|
||||
el?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function commitCatAdd() {
|
||||
if (catCommitLock) return;
|
||||
if (!catManage.adding) return;
|
||||
const name = catManage.newName.trim();
|
||||
if (!name) { catManage.adding = false; catManage.newName = ""; return; }
|
||||
catCommitLock = true;
|
||||
try {
|
||||
await Apis.CustomDeptCategory.customDeptCategoryAdd({
|
||||
data: { schoolId: props.schoolId as number, name },
|
||||
cacheFor: null,
|
||||
});
|
||||
Message.success("已新增分类");
|
||||
catManage.adding = false;
|
||||
catManage.newName = "";
|
||||
await reloadCatAll();
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "新增分类失败");
|
||||
} finally {
|
||||
catCommitLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeCategory(c: CustomDeptCategoryVo) {
|
||||
if (hasDepartments(c)) {
|
||||
// 服务端同样会拒绝;前端先挡一道并说清原因,避免用户点了才知道
|
||||
Message.warning(`分类「${c.name}」下还有 ${c.departmentCount} 个部门,请先移动或删除这些部门`);
|
||||
return;
|
||||
}
|
||||
MyConfirm({
|
||||
title: "删除分类",
|
||||
msg: `确定删除分类【${c.name}】?`,
|
||||
info: "删除为软删除:分类下的部门必须已先移走,历史引用仍按原名称解析。",
|
||||
async onOk() {
|
||||
await Apis.CustomDeptCategory.customDeptCategoryDelete({ pathParams: { id: c.id as number } });
|
||||
Message.success("已删除");
|
||||
await reloadCatAll();
|
||||
// 删的是当前分类时 loadCategories 会自动切到第一个,这里补一次部门列表
|
||||
await loadDepartments();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onCatSortStart(idx: number) {
|
||||
if (catManage.editIdx !== null || catManage.adding) return;
|
||||
catSortIdx.value = idx;
|
||||
}
|
||||
function onCatSortOver(idx: number) {
|
||||
if (catSortIdx.value === null || catSortIdx.value === idx) return;
|
||||
catSortOverIdx.value = idx;
|
||||
const ordered = [...catManage.list];
|
||||
const [row] = ordered.splice(catSortIdx.value, 1);
|
||||
ordered.splice(idx, 0, row);
|
||||
catManage.list = ordered;
|
||||
catSortIdx.value = idx;
|
||||
}
|
||||
function onCatSortDrop() {
|
||||
catSortIdx.value = null;
|
||||
}
|
||||
function onCatSortEnd() {
|
||||
catSortIdx.value = null;
|
||||
catSortOverIdx.value = null;
|
||||
}
|
||||
|
||||
async function saveCatOrder() {
|
||||
if (!catManage.list.length) {
|
||||
catManage.visible = false;
|
||||
return;
|
||||
}
|
||||
catManage.saving = true;
|
||||
try {
|
||||
await Apis.CustomDeptCategory.customDeptCategorySort({
|
||||
data: { schoolId: props.schoolId as number, ids: catManage.list.map((c) => c.id as number) },
|
||||
cacheFor: null,
|
||||
});
|
||||
// 左栏直接采用弹框里的顺序,避免重取丢选中态
|
||||
categories.value = catManage.list.map((c) => ({ ...c }));
|
||||
catManage.visible = false;
|
||||
Message.success("分类顺序已保存");
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "分类排序保存失败");
|
||||
await loadCategories();
|
||||
} finally {
|
||||
catManage.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 部门排序(弹框拖动) ================= */
|
||||
|
||||
const sortVisible = ref(false);
|
||||
@@ -343,100 +577,18 @@ async function saveSort() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 分类拖动排序 ================= */
|
||||
|
||||
const catDragIdx = ref<number | null>(null);
|
||||
|
||||
function onCatDragStart(idx: number) {
|
||||
catDragIdx.value = idx;
|
||||
}
|
||||
function onCatDragOver(idx: number) {
|
||||
if (catDragIdx.value === null || catDragIdx.value === idx) return;
|
||||
const ordered = [...categories.value];
|
||||
const [row] = ordered.splice(catDragIdx.value, 1);
|
||||
ordered.splice(idx, 0, row);
|
||||
categories.value = ordered;
|
||||
catDragIdx.value = idx;
|
||||
}
|
||||
function onCatDrop() {
|
||||
if (catDragIdx.value === null) return;
|
||||
catDragIdx.value = null;
|
||||
persistCategoryOrder();
|
||||
}
|
||||
function onCatDragEnd() {
|
||||
catDragIdx.value = null;
|
||||
}
|
||||
async function persistCategoryOrder() {
|
||||
try {
|
||||
await Apis.CustomDeptCategory.customDeptCategorySort({
|
||||
data: { schoolId: props.schoolId as number, ids: categories.value.map((c) => c.id as number) },
|
||||
cacheFor: null,
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.warn(e?.message || "分类排序保存失败");
|
||||
await loadCategories();
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 分类操作 ================= */
|
||||
|
||||
const hasDepartments = (c: CustomDeptCategoryVo) => Number(c.departmentCount || 0) > 0;
|
||||
|
||||
function openCategoryCreate() {
|
||||
Object.assign(nameModal, {
|
||||
kind: "category", isEdit: false, id: null, name: "", categoryId: null,
|
||||
title: "新增分类", placeholder: "如:审核部门 / 事项部门", visible: true,
|
||||
});
|
||||
}
|
||||
|
||||
function openCategoryRename(c: CustomDeptCategoryVo) {
|
||||
Object.assign(nameModal, {
|
||||
kind: "category", isEdit: true, id: c.id ?? null, name: c.name || "", categoryId: null,
|
||||
title: `重命名分类「${c.name}」`, placeholder: "分类名称", visible: true,
|
||||
});
|
||||
}
|
||||
|
||||
function removeCategory(c: CustomDeptCategoryVo) {
|
||||
if (hasDepartments(c)) {
|
||||
// 服务端同样会拒绝;前端先挡一道并说清原因,避免用户点了才知道
|
||||
Message.warning(`分类「${c.name}」下还有 ${c.departmentCount} 个部门,请先移动或删除这些部门`);
|
||||
return;
|
||||
}
|
||||
MyConfirm({
|
||||
title: "删除分类",
|
||||
msg: `确定删除分类【${c.name}】?`,
|
||||
info: "删除为软删除:分类下的部门必须已先移走,历史引用仍按原名称解析。",
|
||||
async onOk() {
|
||||
await Apis.CustomDeptCategory.customDeptCategoryDelete({ pathParams: { id: c.id as number } });
|
||||
Message.success("已删除");
|
||||
await loadCategories();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 上移/下移:整表重排后一次性提交(服务端按传入顺序落 sort_no) */
|
||||
async function moveCategory(idx: number, delta: number) {
|
||||
const target = idx + delta;
|
||||
if (target < 0 || target >= categories.value.length) return;
|
||||
const ordered = [...categories.value];
|
||||
const [row] = ordered.splice(idx, 1);
|
||||
ordered.splice(target, 0, row);
|
||||
categories.value = ordered;
|
||||
await persistCategoryOrder();
|
||||
}
|
||||
|
||||
/* ================= 部门操作 ================= */
|
||||
|
||||
function openDeptCreate() {
|
||||
Object.assign(nameModal, {
|
||||
kind: "dept", isEdit: false, id: null, name: "", categoryId: activeCategoryId.value,
|
||||
isEdit: false, id: null, name: "", categoryId: activeCategoryId.value,
|
||||
title: "新增部门", placeholder: "如:校长室 / 教务处", visible: true,
|
||||
});
|
||||
}
|
||||
|
||||
function openDeptEdit(d: CustomDepartmentVo) {
|
||||
Object.assign(nameModal, {
|
||||
kind: "dept", isEdit: true, id: d.id ?? null, name: d.name || "", categoryId: d.categoryId ?? null,
|
||||
isEdit: true, id: d.id ?? null, name: d.name || "", categoryId: d.categoryId ?? null,
|
||||
title: `编辑部门「${d.name}」`, placeholder: "部门名称", visible: true,
|
||||
});
|
||||
}
|
||||
@@ -449,24 +601,6 @@ async function submitNameModal() {
|
||||
}
|
||||
nameModal.saving = true;
|
||||
try {
|
||||
if (nameModal.kind === "category") {
|
||||
if (nameModal.isEdit) {
|
||||
await Apis.CustomDeptCategory.customDeptCategoryEdit({
|
||||
pathParams: { id: nameModal.id as number },
|
||||
data: { schoolId: props.schoolId as number, name },
|
||||
cacheFor: null,
|
||||
});
|
||||
} else {
|
||||
await Apis.CustomDeptCategory.customDeptCategoryAdd({
|
||||
data: { schoolId: props.schoolId as number, name },
|
||||
cacheFor: null,
|
||||
});
|
||||
}
|
||||
nameModal.visible = false;
|
||||
Message.success("已保存");
|
||||
await loadCategories();
|
||||
return;
|
||||
}
|
||||
if (nameModal.isEdit) {
|
||||
await Apis.CustomDept.customDeptEdit({
|
||||
pathParams: { id: nameModal.id as number },
|
||||
@@ -609,6 +743,7 @@ async function refreshAfterMemberChange() {
|
||||
padding: 26px 12px;
|
||||
text-align: center;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-3, #98a1b2);
|
||||
border: 1px dashed var(--line, #e9edf5);
|
||||
border-radius: var(--r, 10px);
|
||||
@@ -619,12 +754,14 @@ async function refreshAfterMemberChange() {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 左列:分类平铺列表 */
|
||||
/* ================= 左列:分类轻量列表 ================= */
|
||||
.cat-col {
|
||||
flex: 0 0 260px;
|
||||
flex: 0 0 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
border-right: 1px solid var(--line, #eef1f7);
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.cat-list {
|
||||
@@ -636,32 +773,40 @@ async function refreshAfterMemberChange() {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.cat-item {
|
||||
/* 参考应用管理的左栏分类:无边框卡片感,选中靠浅底 + 左侧细蓝条 */
|
||||
.cat {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid var(--line, #e9edf5);
|
||||
border-radius: var(--r, 8px);
|
||||
cursor: pointer;
|
||||
gap: 8px;
|
||||
height: 38px;
|
||||
padding: 0 10px 0 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text, #1e2635);
|
||||
transition: border-color .15s, background .15s;
|
||||
|
||||
&.dragging {
|
||||
opacity: .5;
|
||||
}
|
||||
cursor: pointer;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
|
||||
.cat-item:hover {
|
||||
border-color: #dbe2fb;
|
||||
background: #f7f9ff;
|
||||
.cat:hover {
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.cat-item.is-active {
|
||||
border-color: var(--brand, #577afb);
|
||||
background: #f2f5ff;
|
||||
.cat.is-active {
|
||||
background: var(--brand-weak, #f0f4ff);
|
||||
color: var(--brand, #577afb);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cat.is-active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 9px;
|
||||
bottom: 9px;
|
||||
width: 2px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
background: var(--brand, #577afb);
|
||||
}
|
||||
|
||||
.cat-name {
|
||||
@@ -678,58 +823,12 @@ async function refreshAfterMemberChange() {
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.cat-ops {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* 拖动把手:默认半透明,hover 变清晰 */
|
||||
.grip {
|
||||
flex: 0 0 auto;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
color: #c4c9d2;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.cat-item:hover .grip,
|
||||
.sort-item:hover .grip {
|
||||
color: #8b96a8;
|
||||
}
|
||||
|
||||
.op {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
color: var(--text-2, #5a6478);
|
||||
}
|
||||
|
||||
.op:hover {
|
||||
background: #e8ecfa;
|
||||
.cat.is-active .cat-count {
|
||||
color: var(--brand, #577afb);
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.op.is-disabled {
|
||||
color: #d3d9e6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.op.is-disabled:hover {
|
||||
background: transparent;
|
||||
color: #d3d9e6;
|
||||
}
|
||||
|
||||
/* 右列:部门表格 */
|
||||
/* ================= 右列:部门表格 ================= */
|
||||
.dept-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -755,15 +854,42 @@ async function refreshAfterMemberChange() {
|
||||
}
|
||||
}
|
||||
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
/* 人员列:姓名区可截断,「+N」固定不收缩 */
|
||||
.mem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
color: var(--text-2, #5a6478);
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
|
||||
.mem-cell:hover {
|
||||
.mem .names {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mem .more {
|
||||
flex: 0 0 auto;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mem-cell:hover .names {
|
||||
color: var(--brand, #577afb);
|
||||
border-bottom-color: #c9d4f7;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.mem-plain {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dim {
|
||||
@@ -771,12 +897,37 @@ async function refreshAfterMemberChange() {
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
.sta-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
/* 状态开关:与系统部门同一套胶囊 */
|
||||
:deep(.sw-mini) {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 28px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
background: #d3d9e6;
|
||||
transition: background .15s;
|
||||
vertical-align: middle;
|
||||
|
||||
.sta-tag:hover {
|
||||
opacity: .82;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(30, 38, 53, .2);
|
||||
transition: left .15s;
|
||||
}
|
||||
|
||||
&.is-on {
|
||||
background: var(--brand, #577afb);
|
||||
|
||||
&::after {
|
||||
left: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lnk {
|
||||
@@ -805,7 +956,7 @@ async function refreshAfterMemberChange() {
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
/* ================= 排序弹框 ================= */
|
||||
/* ================= 排序 / 分类管理弹框 ================= */
|
||||
.sort-tip {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -843,6 +994,30 @@ async function refreshAfterMemberChange() {
|
||||
border-color: var(--brand, #577afb);
|
||||
box-shadow: 0 0 0 2px rgba(87, 122, 251, .16);
|
||||
}
|
||||
|
||||
&.editing {
|
||||
border-color: var(--brand, #577afb);
|
||||
background: #fbfcff;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.grip {
|
||||
flex: 0 0 auto;
|
||||
font-style: normal;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
color: #c4c9d2;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.sort-item:hover .grip {
|
||||
color: #8b96a8;
|
||||
}
|
||||
|
||||
.sort-item .sno {
|
||||
@@ -866,4 +1041,44 @@ async function refreshAfterMemberChange() {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.snm-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sort-item .cCnt {
|
||||
flex: 0 0 auto;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
.sort-item .row-ops {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sort-item .row-ops .lnk {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sort-item .row-ops .lnk.is-disabled {
|
||||
color: #b3bccb;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
:row-class-name="rowClassName" :loading="false" no-data-text="暂无部门">
|
||||
<template #name="{ row }">
|
||||
<span class="nmcell" :style="{ paddingLeft: row.level * 20 + 'px' }">
|
||||
<i v-if="row.kidCount" class="tw" @click.stop="toggleRow(row.id)">{{ row.open ? '▾' : '▸' }}</i>
|
||||
<i v-if="row._kids.length" class="tw" @click.stop="toggleRow(row.id)">{{ row.open ? '▾' : '▸' }}</i>
|
||||
<i v-else class="tw blank"></i>
|
||||
<span class="nm" :class="{ root: row.level === 0 }" :title="row.name">{{ row.name }}</span>
|
||||
<span v-if="row.kidCount" class="cnt">{{ row.kidCount }} 个下级</span>
|
||||
@@ -62,15 +62,18 @@
|
||||
</template>
|
||||
<template #members="{ row }">
|
||||
<Tooltip v-if="row.memberCount" placement="top" max-width="360" :content="memberTip(row)">
|
||||
<span class="mem-cell" @click="openMembers(row)">{{ memberText(row) }}</span>
|
||||
<span class="mem" :class="memberClickable(row) ? 'mem-cell' : 'mem-plain'"
|
||||
@click="onMemberClick(row)">
|
||||
<span class="names">{{ memberHead(row) }}</span>
|
||||
<span v-if="memberRest(row)" class="more">+{{ memberRest(row) }}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span v-else class="dim">无成员</span>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag type="border" :color="row.status === 1 ? 'error' : 'success'" class="sta-tag"
|
||||
:title="row.status === 1 ? '点击启用' : '点击停用'" @click="onToggle(row.raw)">
|
||||
{{ row.status === 1 ? '已停用' : '已启用' }}
|
||||
</Tag>
|
||||
<i class="sw-mini" :class="{ 'is-on': row.status !== 1 }"
|
||||
:title="row.status === 1 ? '点击启用' : '点击停用'"
|
||||
@click="onToggle(row.raw)"></i>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="table-foot">共 {{ flatRows.length }} 个部门</div>
|
||||
@@ -173,7 +176,12 @@ interface DeptRow {
|
||||
open: boolean;
|
||||
no: number;
|
||||
raw: any;
|
||||
children: DeptRow[];
|
||||
/**
|
||||
* 子级只给自己用。
|
||||
* ⚠️ 绝不能叫 children:iView Table 发现行上有 children 就切「树形数据模式」(还要求 row-key),
|
||||
* 会和这里自画的缩进 + 折叠打架,表现是整表空白(关掉包含子组后 children 为空数组才恢复)。
|
||||
*/
|
||||
_kids: DeptRow[];
|
||||
}
|
||||
|
||||
/** 表格树:一级部门来自各教师组下的直接子节点,子级递归;「包含子组」关闭时不带子级 */
|
||||
@@ -190,7 +198,7 @@ const deptRows = computed<DeptRow[]>(() => {
|
||||
});
|
||||
|
||||
function toRow(d: any, parentName: string, level: number): DeptRow {
|
||||
const kids: any[] = includeOn.value ? (d.children || []) : [];
|
||||
const kids: any[] = d.children || [];
|
||||
return {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
@@ -200,11 +208,12 @@ function toRow(d: any, parentName: string, level: number): DeptRow {
|
||||
isStudent: d.type === STUDENT_GROUP_TYPE,
|
||||
memberCount: Number(d.memberCount ?? 0),
|
||||
memberNames: Array.isArray(d.memberNames) ? d.memberNames : [],
|
||||
// 下级数始终按真实层级给:关闭包含子组时只收起子行,徽标仍要能看出「这个部门有下级」
|
||||
kidCount: kids.length,
|
||||
open: !closedIds.value.has(d.id),
|
||||
no: 0,
|
||||
raw: d,
|
||||
children: kids.map((k) => toRow(k, d.name, level + 1)),
|
||||
_kids: includeOn.value ? kids.map((k) => toRow(k, d.name, level + 1)) : [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,7 +223,7 @@ const flatRows = computed<DeptRow[]>(() => {
|
||||
const walk = (list: DeptRow[]) => {
|
||||
list.forEach((row) => {
|
||||
out.push(row);
|
||||
if (row.kidCount && row.open) walk(row.children);
|
||||
if (row.open && row._kids.length) walk(row._kids);
|
||||
});
|
||||
};
|
||||
walk(deptRows.value);
|
||||
@@ -224,28 +233,40 @@ const flatRows = computed<DeptRow[]>(() => {
|
||||
|
||||
/** iView Table 列定义:层级用「部门名称」列的缩进 + 折叠表达,不用 Table 自带树(避免样式不可控) */
|
||||
const sysColumns: any[] = [
|
||||
{ title: "序号", type: "index", width: 80, align: "center" },
|
||||
{ title: "部门名称", slot: "name", minWidth: 300 },
|
||||
{ title: "上级", key: "parentName", width: 160 },
|
||||
{ title: "人员", slot: "members", minWidth: 320 },
|
||||
{ title: "状态", slot: "status", width: 120, align: "center" },
|
||||
{ title: "序号", type: "index", width: 70, align: "center" },
|
||||
{ title: "部门名称", slot: "name", width: 236 },
|
||||
{ title: "上级", key: "parentName", width: 130 },
|
||||
{ title: "人员", slot: "members", minWidth: 200 },
|
||||
{ title: "状态", slot: "status", width: 90, align: "center" },
|
||||
];
|
||||
/** 停用行整行灰化 */
|
||||
function rowClassName(row: any) {
|
||||
return row?.status === 1 ? "is-off" : "";
|
||||
}
|
||||
|
||||
/** 人员列:前 3 个姓名 + 剩余数;无姓名时退回「N 人」 */
|
||||
function memberText(row: DeptRow) {
|
||||
/** 人员列:最多平铺 10 个姓名,超出用「+N」提示;已全部显示(≤10 人)时不可点 */
|
||||
const MEMBER_PREVIEW = 10;
|
||||
function memberHead(row: DeptRow) {
|
||||
// 姓名预览还没回来(后端未升级 / 未重启)时先退回人数,避免出现空单元格
|
||||
if (!row.memberNames.length) return `${row.memberCount} 人`;
|
||||
const head = row.memberNames.slice(0, 3).join("、");
|
||||
const rest = row.memberCount - Math.min(3, row.memberNames.length);
|
||||
return rest > 0 ? `${head} +${rest}` : head;
|
||||
return row.memberNames.slice(0, MEMBER_PREVIEW).join("、");
|
||||
}
|
||||
function memberRest(row: DeptRow) {
|
||||
if (!row.memberNames.length) return 0;
|
||||
return Math.max(0, row.memberCount - Math.min(MEMBER_PREVIEW, row.memberNames.length));
|
||||
}
|
||||
/** 只有列不完时才值得点开弹窗(全在表格里看完了,弹窗没有增量信息) */
|
||||
function memberClickable(row: DeptRow) {
|
||||
return row.memberCount > MEMBER_PREVIEW;
|
||||
}
|
||||
function onMemberClick(row: DeptRow) {
|
||||
if (memberClickable(row)) openMembers(row);
|
||||
}
|
||||
function memberTip(row: DeptRow) {
|
||||
if (!row.memberNames.length) return `共 ${row.memberCount} 人 点击查看全部`;
|
||||
const list = row.memberNames.join("、");
|
||||
const more = row.memberCount > row.memberNames.length ? `…(仅预览前 ${row.memberNames.length} 人)` : "";
|
||||
return `共 ${row.memberCount} 人:${list}${more} 点击查看全部`;
|
||||
return `共 ${row.memberCount} 人:${list}${more}${memberClickable(row) ? " 点击查看全部" : ""}`;
|
||||
}
|
||||
|
||||
/* ================= 人员弹窗(查看全部,分页) ================= */
|
||||
@@ -550,6 +571,7 @@ async function loadSchoolScope() {
|
||||
border-radius: 8px;
|
||||
background: #d3d9e6;
|
||||
transition: background .15s;
|
||||
vertical-align: middle;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
@@ -645,15 +667,43 @@ async function loadSchoolScope() {
|
||||
color: #a3adbd;
|
||||
}
|
||||
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
/* 人员列:姓名区可截断,「+N」固定不收缩(名字太长时也得看得出还有没列完的人) */
|
||||
.mem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
color: var(--text-2, #5a6478);
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
|
||||
.mem-cell:hover {
|
||||
.mem .names {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mem .more {
|
||||
flex: 0 0 auto;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-3, #98a1b2);
|
||||
}
|
||||
|
||||
/* 只有超过 10 人才可点(全部列完再弹窗没有增量信息) */
|
||||
.mem-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mem-cell:hover .names {
|
||||
color: var(--brand, #577afb);
|
||||
border-bottom-color: #c9d4f7;
|
||||
text-decoration-line: underline;
|
||||
text-decoration-style: dashed;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.mem-plain {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.dim {
|
||||
@@ -661,16 +711,7 @@ async function loadSchoolScope() {
|
||||
font-size: 11.5px;
|
||||
}
|
||||
|
||||
/* 状态标签可点:hover 给一点反馈,暗示能切换 */
|
||||
.sta-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sta-tag:hover {
|
||||
opacity: .82;
|
||||
}
|
||||
|
||||
|
||||
/* 状态开关:复用「包含子组」那套胶囊;表格里要跟文字垂直居中 */
|
||||
.table-foot {
|
||||
flex: 0 0 auto;
|
||||
padding: 10px 2px;
|
||||
|
||||
Reference in New Issue
Block a user