feat(dept): 部门管理新增 CUSTOM 自定义部门页签
build / build (push) Successful in 2m7s

- index.vue 来源切换「系统部门|自定义部门」,SYSTEM 原 DOM 原样 v-if 包装零逻辑改动,学校上下文共用
- CustomDeptPanel/CustomDeptCard:分类增删改排序(非空禁删)、部门增改移启停软删(风险二次确认)
- CustomMemberModal:候选走 CandidateUsers(服务端同口径排除学生组),搜索+勾选+批量添加
- apiDefinitions/globals.d.ts 补 17 个 /api 契约(key 与后端 operationId 严格一致)
- 遗留:npm run api:gen 待后端可起环境补跑校验
This commit is contained in:
chenx
2026-10-22 09:36:18 +08:00
parent b8c55a0654
commit 83a73dfcc8
6 changed files with 1660 additions and 16 deletions
+199
View File
@@ -0,0 +1,199 @@
<template>
<article class="cdept-card" :class="{ 'is-off': !dept.enabled }">
<div class="row-1">
<span class="status-dot" :class="{ 'is-off': !dept.enabled }"></span>
<h3 class="card-name" :title="dept.name">{{ dept.name }}</h3>
<span class="sw" :class="{ 'is-off': !dept.enabled }" :title="dept.enabled ? '点击停用' : '点击启用'"
@click="emit('toggle', dept)"></span>
</div>
<div class="row-2">
<span class="sub">{{ memberText }}</span>
<span class="ops">
<i class="op" :class="{ 'is-disabled': !canMoveUp }" title="上移" @click="emit('moveUp')">↑</i>
<i class="op" :class="{ 'is-disabled': !canMoveDown }" title="下移" @click="emit('moveDown')">↓</i>
<a class="link" @click="emit('members', dept)">成员</a>
<a class="link" @click="emit('edit', dept)">编辑</a>
<a class="link danger" @click="emit('remove', dept)">删除</a>
</span>
</div>
</article>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import type { CustomDepartmentVo } from "@/api/globals";
/**
* CUSTOM 部门卡片:与 SYSTEM 的 DeptCard 同款外观,但字段来自 ub_custom_department。
* 停用在管理端照常可见(灰态),软删的部门由后端列表过滤、不会走到这里。
*/
const props = defineProps<{
dept: CustomDepartmentVo;
canMoveUp?: boolean;
canMoveDown?: boolean;
}>();
const emit = defineEmits<{
(e: "toggle", dept: CustomDepartmentVo): void;
(e: "edit", dept: CustomDepartmentVo): void;
(e: "remove", dept: CustomDepartmentVo): void;
(e: "members", dept: CustomDepartmentVo): void;
(e: "moveUp"): void;
(e: "moveDown"): void;
}>();
const memberText = computed(() => {
const count = Number(props.dept?.memberCount ?? 0);
if (!props.dept?.enabled) return count ? `已停用 · ${count} 人` : "已停用 · 无成员";
return count ? `${count} 人` : "无成员(可先建部门再配人)";
});
</script>
<style scoped lang="scss">
.cdept-card {
display: flex;
flex-direction: column;
gap: 9px;
padding: 13px 14px;
background: var(--panel, #fff);
border: 1px solid var(--line, #e9edf5);
border-radius: var(--r, 10px);
box-shadow: 0 1px 2px rgba(30, 38, 53, .03);
transition: border-color .15s, box-shadow .15s;
}
.cdept-card:hover {
border-color: #dbe2fb;
box-shadow: 0 6px 18px rgba(30, 38, 53, .07);
}
.cdept-card.is-off {
background: #fbfcfd;
border-style: dashed;
}
.row-1 {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
flex: 0 0 6px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--green, #20a162);
}
.status-dot.is-off {
background: #cbd2df;
}
.card-name {
flex: 1;
min-width: 0;
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.4;
color: var(--text, #1e2635);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cdept-card.is-off .card-name {
color: var(--text-3, #98a1b2);
}
.row-2 {
display: flex;
align-items: center;
gap: 8px;
min-height: 26px;
margin-top: auto;
padding-top: 9px;
border-top: 1px dashed var(--line-2, #f0f3f9);
font-size: 12px;
color: var(--text-3, #98a1b2);
}
.row-2 .sub {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ops {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 8px;
}
.link {
color: var(--brand, #577afb);
cursor: pointer;
}
.link.danger {
color: #e54545;
}
.op {
font-style: normal;
font-size: 12px;
line-height: 18px;
width: 18px;
text-align: center;
border-radius: 4px;
color: var(--text-2, #5a6478);
cursor: pointer;
}
.op:hover {
background: #e8ecfa;
color: var(--brand, #577afb);
}
.op.is-disabled {
color: #d3d9e6;
cursor: not-allowed;
}
/* 启停开关(与 SYSTEM 卡片同一套视觉) */
.sw {
position: relative;
flex: 0 0 auto;
width: 36px;
height: 20px;
border-radius: 10px;
background: var(--brand, #577afb);
cursor: pointer;
transition: background .15s;
}
.sw::after {
content: '';
position: absolute;
top: 2px;
left: 18px;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(30, 38, 53, .22);
transition: left .15s;
}
.sw.is-off {
background: #d3d9e6;
}
.sw.is-off::after {
left: 2px;
}
</style>