@@ -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`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user