图标
build / build (push) Successful in 4m19s

This commit is contained in:
wanghu
2026-10-20 16:20:47 +08:00
parent b1f065a790
commit 0f10467ebb
3 changed files with 127 additions and 52 deletions
+1 -1
View File
@@ -193,7 +193,7 @@ class ClassroomApi {
floors(buildingId: number) { floors(buildingId: number) {
return get<number[]>(`${CLASSROOM}/Floors`, { buildingId }); return get<number[]>(`${CLASSROOM}/Floors`, { buildingId });
} }
/** 全校教室扁平列表(空间筛选树数据源,轻量字段) */ /** 全校教室扁平列表(楼层筛选树数据源,轻量字段) */
tree() { tree() {
return get<ClassroomTreeRow[]>(`${CLASSROOM}/Tree`); return get<ClassroomTreeRow[]>(`${CLASSROOM}/Tree`);
} }
+1 -1
View File
@@ -114,7 +114,7 @@ export interface Classroom {
headTeacher?: string | null; headTeacher?: string | null;
} }
/** 空间筛选树数据行(classroom/Tree 返回的轻量教室行,前端按 校区→楼栋→楼层 组树) */ /** 楼层筛选树数据行(classroom/Tree 返回的轻量教室行,前端按 校区→楼栋→楼层 组树) */
export interface ClassroomTreeRow { export interface ClassroomTreeRow {
serID: number; serID: number;
campusId: number; campusId: number;
+125 -50
View File
@@ -23,7 +23,7 @@ const GRADE_LABELS: Record<number, string> = { 1: "高一", 2: "高二", 3: "高
/** /**
* 教室管理(核心页)· 三栏布局 * 教室管理(核心页)· 三栏布局
* *
* 信息层级:左侧「空间筛选」树(校区→楼栋→楼层→教室,点击节点即切换列表作用域) * 信息层级:左侧「楼层筛选」树(校区→楼栋→楼层→教室,点击节点即切换列表作用域)
* → 中间教室列表(搜索 / 筛选 / 排序 / 分页 / 多选批量操作 / 批量导出) * → 中间教室列表(搜索 / 筛选 / 排序 / 分页 / 多选批量操作 / 批量导出)
* → 右侧选中教室的详情面板(字段 + 布局示意图 + 编辑入口)。 * → 右侧选中教室的详情面板(字段 + 布局示意图 + 编辑入口)。
* *
@@ -911,10 +911,10 @@ onMounted(async () => {
<template> <template>
<div class="ac-page"> <div class="ac-page">
<div class="cm-layout"> <div class="cm-layout">
<!-- ========== 左:空间筛选树 ========== --> <!-- ========== 左:楼层筛选树 ========== -->
<aside class="ac-card cm-tree"> <aside class="ac-card cm-tree">
<div class="ac-card__head cm-pane-head"> <div class="ac-card__head cm-pane-head">
<div class="ac-card__title">空间筛选</div> <div class="ac-card__title">楼层筛选</div>
<span class="pane-sub">共 {{ treeRooms.length }} 间</span> <span class="pane-sub">共 {{ treeRooms.length }} 间</span>
</div> </div>
<div class="cm-tree__body"> <div class="cm-tree__body">
@@ -952,7 +952,14 @@ onMounted(async () => {
<span class="tree-caret" :class="{ 'is-open': isOpen(b.key) }"> <span class="tree-caret" :class="{ 'is-open': isOpen(b.key) }">
<Icon type="ios-arrow-forward" /> <Icon type="ios-arrow-forward" />
</span> </span>
<span class="tree-icon"><Icon type="ios-folder" /></span> <span class="tree-icon">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M6 2h12v20H6V2zm2 2.5v2.5h2.4V4.5H8zm5.6 0v2.5H16V4.5h-2.4zM8 9v2.5h2.4V9H8zm5.6 0v2.5H16V9h-2.4zM8 13.5V16h2.4v-2.5H8zm5.6 0V16H16v-2.5h-2.4zM10.5 22v-5h3v5h-3z"
/>
</svg>
</span>
<span class="tree-label" :title="b.title">{{ b.title }}</span> <span class="tree-label" :title="b.title">{{ b.title }}</span>
<span v-if="b.sub" class="tree-sub">{{ b.sub }}</span> <span v-if="b.sub" class="tree-sub">{{ b.sub }}</span>
<span class="tree-count">{{ b.count }}</span> <span class="tree-count">{{ b.count }}</span>
@@ -1005,7 +1012,18 @@ onMounted(async () => {
> >
<Icon type="ios-checkmark" class="tree-check__on" /> <Icon type="ios-checkmark" class="tree-check__on" />
</span> </span>
<span class="tree-dot" :class="r.status === 'open' ? 'is-open' : 'is-closed'"></span> <span
class="tree-door"
:class="r.status === 'open' ? 'is-open' : 'is-closed'"
:title="r.status === 'open' ? '开放' : '关闭'"
>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M6.5 3h11v18h-11zM14.7 11.15a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3zM4 21h16v1.5H4z"
/>
</svg>
</span>
<span class="tree-label" :title="r.title">{{ r.title }}</span> <span class="tree-label" :title="r.title">{{ r.title }}</span>
</div> </div>
</template> </template>
@@ -1132,41 +1150,49 @@ onMounted(async () => {
</div> </div>
</transition> </transition>
<Table <div class="cm-table-wrap">
ref="tableRef" <!-- 自绘加载层:Table 内置 Spin 的淡出过渡在渲染帧冻结时会卡住并拦截点击,
:loading="loadingRooms" 这里用 v-show 直接切 display,pointer-events 不拦鼠标 -->
:columns="columns" <div v-show="loadingRooms" class="cm-table-loading" aria-hidden="true">
:data="rooms.list" <span class="cm-table-loading__dot"></span>
row-key="serID" <span>加载中…</span>
border </div>
:no-data-text="''" <Table
:no-filtered-data-text="''" ref="tableRef"
@on-sort-change="onSortChange" :columns="columns"
@on-selection-change="(rows: Classroom[]) => (selection = rows)" :data="rooms.list"
@on-row-click="(row: Classroom) => { focusId = row.serID; detailVisible = true; }" row-key="serID"
> border
<template #floor="{ row }"> :no-data-text="''"
{{ row.floor }}层 :no-filtered-data-text="''"
</template> @on-sort-change="onSortChange"
<template #status="{ row }"> @on-selection-change="(rows: Classroom[]) => (selection = rows)"
<Tooltip content="点击切换 开放 / 关闭" placement="top" transfer> @on-row-click="(row: Classroom) => { focusId = row.serID; detailVisible = true; }"
<span >
class="status-toggle" <template #floor="{ row }">
:class="{ 'is-busy': togglingId === row.serID }" {{ row.floor }}层
@click.stop="toggleStatus(row)" </template>
> <template #status="{ row }">
<Tag :color="row.status === 'open' ? 'success' : 'error'"> <Tooltip content="点击切换 开放 / 关闭" placement="top" transfer>
{{ row.status === "open" ? "开放" : "关闭" }} <span class="status-toggle" :class="{ 'is-busy': togglingId === row.serID }">
</Tag> <!-- 点击必须绑在 Tag 上:Tag 根节点自带 @click.stop,
</span> 事件冒泡不到外层 span -->
</Tooltip> <Tag
</template> :color="row.status === 'open' ? 'success' : 'error'"
<template #action="{ row }"> @click.stop="toggleStatus(row)"
<span class="row-action row-action--detail" @click.stop="focusId = row.serID; detailVisible = true;">详情</span> >
<span class="row-action" @click.stop="openEdit(row)">编辑</span> {{ row.status === "open" ? "开放" : "关闭" }}
<span class="row-action row-action--danger" @click.stop="removeRows([row])">删除</span> </Tag>
</template> </span>
</Table> </Tooltip>
</template>
<template #action="{ row }">
<span class="row-action row-action--detail" @click.stop="focusId = row.serID; detailVisible = true;">详情</span>
<span class="row-action" @click.stop="openEdit(row)">编辑</span>
<span class="row-action row-action--danger" @click.stop="removeRows([row])">删除</span>
</template>
</Table>
</div>
<!-- view-ui-plus 的 Table 没有 empty 插槽,空态改为表格下方自渲染 <!-- view-ui-plus 的 Table 没有 empty 插槽,空态改为表格下方自渲染
(内置空文案已用 no-data-text / no-filtered-data-text 置空关闭) --> (内置空文案已用 no-data-text / no-filtered-data-text 置空关闭) -->
@@ -1241,7 +1267,7 @@ onMounted(async () => {
<div class="detail-row"> <div class="detail-row">
<span class="detail-row__label">状态</span> <span class="detail-row__label">状态</span>
<span class="detail-row__value"> <span class="detail-row__value">
<Tag :color="focusRoom.status === 'open' ? 'success' : 'error'"> <Tag :color="focusRoom.status === 'open' ? 'success' : 'default'">
{{ focusRoom.status === "open" ? "开放" : "关闭" }} {{ focusRoom.status === "open" ? "开放" : "关闭" }}
</Tag> </Tag>
</span> </span>
@@ -1502,7 +1528,7 @@ onMounted(async () => {
white-space: nowrap; white-space: nowrap;
} }
/* ============================ 空间筛选树 ============================ */ /* ============================ 楼层筛选树 ============================ */
.cm-tree__body { .cm-tree__body {
flex: 1 1 auto; flex: 1 1 auto;
overflow: auto; overflow: auto;
@@ -1605,6 +1631,12 @@ onMounted(async () => {
.ivu-icon { .ivu-icon {
font-size: 15px; font-size: 15px;
} }
> svg {
display: block;
width: 15px;
height: 15px;
}
} }
.tree-icon--campus { .tree-icon--campus {
@@ -1686,19 +1718,21 @@ onMounted(async () => {
} }
} }
.tree-dot { .tree-door {
flex: none; flex: none;
width: 7px; display: inline-flex;
height: 7px; width: 13px;
border-radius: 50%; height: 13px;
margin: 0 3px; color: var(--ac-success);
&.is-open { > svg {
background: var(--ac-success); display: block;
width: 13px;
height: 13px;
} }
&.is-closed { &.is-closed {
background: var(--ac-danger); color: var(--ac-text-disabled);
} }
} }
@@ -1851,6 +1885,47 @@ onMounted(async () => {
padding: 0 10px; padding: 0 10px;
} }
.cm-table-wrap {
position: relative;
}
/* 表格加载层:v-show 直接切 display,不依赖过渡结束;pointer-events 置空,
即使常驻也不会拦截行内按钮/状态标签的点击 */
.cm-table-loading {
position: absolute;
inset: 0;
z-index: 8;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
background: rgba(255, 255, 255, 0.72);
color: var(--ac-text-secondary);
font-size: 13px;
pointer-events: none;
}
.cm-table-loading__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ac-primary);
animation: cm-loading-blink 0.9s ease-in-out infinite;
}
@keyframes cm-loading-blink {
0%,
100% {
opacity: 0.25;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1);
}
}
.status-toggle { .status-toggle {
display: inline-flex; display: inline-flex;
cursor: pointer; cursor: pointer;