feat(documents): 文书库筛选条按院区-科室-状态三级重构

This commit is contained in:
yelan
2026-10-07 11:34:15 +08:00
parent ee7e90ab01
commit 3ab71e4231
5 changed files with 235 additions and 76 deletions
@@ -1,23 +1,33 @@
<script setup lang="ts">
/**
* 文书库的两级筛选条(原型 V10.2 的 #libTabs / #libStatus)。
* 文书库的分级筛选条:院区 → 科室 → 状态。
*
* 类名刻意沿用原型的 lib-tabs / lib-tab / lib-status / lib-st:这套样式在原型里
* 是文档库页面独有的筛选语言(胶囊 + 虚线分隔),与通用 Tabs 组件的观感不同,
* 换成通用组件会把「全部(8)」这类计数排版搞丢。
* 原型 V10.2 只有「科室 + 状态」两行(#libTabs / #libStatus)。这里把科室那一级按
* 「组织架构」的口径拆成两级:先选院区(同一份院区字典、同一个 sortNo 顺序,
* 与架构树的院区顺序一致),再在该院区的科室里筛。院区不做「全部院区」——
* 架构树里也始终落在某一个院区上,两处口径保持一致。
*
* 类名沿用原型的 lib-tabs / lib-tab / lib-status / lib-st:这套样式是文档库页面独有的
* 筛选语言(胶囊 + 虚线分隔),与通用 Tabs 组件的观感不同,换成通用组件会把
* 「全部(8)」这类计数排版搞丢。
*
* 用 <button> 而不是原型的 <span onclick>:保留键盘可达与 aria-selected。
*/
import type { DepartmentTab, DocumentFilterStatus, StatusTab } from '../types'
import type { CampusTab, DepartmentTab, DocumentFilterStatus, StatusTab } from '../types'
defineProps<{
/** 当前院区 id。院区是页面级的取数口径,由页面持有(见 index.vue) */
campus: string
department: string | null
status: DocumentFilterStatus
/** 演示文库没有院区维度,页面会传空数组,这一级整行不渲染 */
campuses: CampusTab[]
departments: DepartmentTab[]
statuses: StatusTab[]
}>()
const emit = defineEmits<{
'select-campus': [value: string]
'update:department': [value: string | null]
'update:status': [value: DocumentFilterStatus]
}>()
@@ -25,40 +35,65 @@ const emit = defineEmits<{
<template>
<div class="lib-filter">
<div class="lib-tabs" role="tablist" aria-label="按科室筛选">
<button
v-for="tab in departments"
:key="tab.key ?? 'all'"
type="button"
class="lib-tab"
:class="{ on: department === tab.key }"
:aria-selected="department === tab.key"
role="tab"
@click="emit('update:department', tab.key)"
>
{{ tab.label }}({{ tab.count }})
</button>
<div v-if="campuses.length" class="lib-row">
<span class="lib-row__label">院区</span>
<div class="lib-tabs" role="tablist" aria-label="按院区筛选">
<button
v-for="tab in campuses"
:key="tab.key"
type="button"
class="lib-tab"
:class="{ on: campus === tab.key }"
:aria-selected="campus === tab.key"
role="tab"
@click="emit('select-campus', tab.key)"
>
{{ tab.label }}
</button>
</div>
</div>
<div class="lib-status" role="tablist" aria-label="按状态筛选">
<button
v-for="tab in statuses"
:key="tab.key"
type="button"
class="lib-st"
:class="{ on: status === tab.key }"
:aria-selected="status === tab.key"
role="tab"
@click="emit('update:status', tab.key)"
>
{{ tab.label }}<template v-if="tab.count"> · {{ tab.count }}</template>
</button>
<!-- 没有院区这一级时(演示模式)科室行就是首行,不再加分隔线 -->
<div class="lib-row" :class="{ 'lib-row--sub': campuses.length }">
<span class="lib-row__label">科室</span>
<div class="lib-tabs" role="tablist" aria-label="按科室筛选">
<button
v-for="tab in departments"
:key="tab.key ?? 'all'"
type="button"
class="lib-tab"
:class="{ on: department === tab.key }"
:aria-selected="department === tab.key"
role="tab"
@click="emit('update:department', tab.key)"
>
{{ tab.label }}({{ tab.count }})
</button>
</div>
</div>
<div class="lib-row lib-row--sub">
<span class="lib-row__label">状态</span>
<div class="lib-status" role="tablist" aria-label="按状态筛选">
<button
v-for="tab in statuses"
:key="tab.key"
type="button"
class="lib-st"
:class="{ on: status === tab.key }"
:aria-selected="status === tab.key"
role="tab"
@click="emit('update:status', tab.key)"
>
{{ tab.label }}<template v-if="tab.count"> · {{ tab.count }}</template>
</button>
</div>
</div>
</div>
</template>
<style scoped>
/* 原型把两条页签包在 .card 里(padding:12px 16px; margin-bottom:12px),照抄 */
/* 原型把筛选条包在 .card 里(padding:12px 16px; margin-bottom:12px),照抄 */
.lib-filter {
padding: 12px 16px;
margin-bottom: 12px;
@@ -67,12 +102,40 @@ const emit = defineEmits<{
box-shadow: var(--sh);
}
/* 原型 .lib-tabs:flex-wrap + gap 8,块间距靠 margin-bottom 交给外层 */
.lib-tabs {
/*
* 每一级筛选占一行:行首一个小标签写明「这一行筛的是什么」。
* 三行胶囊外观相近,不标出来就分不清哪行是院区、哪行是科室。
*/
.lib-row {
display: flex;
gap: 10px;
align-items: flex-start;
}
/* 下级与上级之间用一条虚线分开(原型里科室与状态之间就是这条线) */
.lib-row--sub {
padding-top: 10px;
margin-top: 10px;
border-top: 1px dashed var(--line);
}
/* 标签与第一行胶囊(约 29px 高)视觉居中 */
.lib-row__label {
flex: 0 0 auto;
padding-top: 6px;
color: var(--mut);
font-size: 12px;
line-height: 1.4;
white-space: nowrap;
}
.lib-tabs,
.lib-status {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
min-width: 0;
}
.lib-tab {
@@ -99,15 +162,7 @@ const emit = defineEmits<{
background: var(--brand);
}
/* 原型 .lib-status:与科室行之间用一条虚线分开,表达「同一维度下的第二级筛选」 */
.lib-status {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding-top: 10px;
border-top: 1px dashed var(--line);
}
/* 状态是比科室更细的一档,用描边胶囊而不是实底,视觉上轻一级 */
.lib-st {
display: inline-flex;
align-items: center;
@@ -149,8 +149,6 @@ const togglePermission = computed(() =>
历史记录
</button>
<span class="ft-spacer" />
<button
v-if="toggleAction"
v-permission="togglePermission"
@@ -326,21 +324,18 @@ const togglePermission = computed(() =>
-webkit-line-clamp: 3;
}
/* 按钮组不做左右两侧分栏:从左到右依次排布(预览 / 编辑属性 / 编辑正文 / 历史记录 / 停用 / 发起) */
.tpl-ft {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
justify-content: flex-start;
padding-top: 10px;
margin-top: 4px;
border-top: 1px solid var(--line);
}
/* 把状态动作与发起推到右侧,左侧四个入口(预览/属性/正文/历史)成一组 */
.ft-spacer {
flex: 1 1 auto;
}
.card-button {
padding: 4px 9px;
color: var(--ink);
@@ -366,10 +361,4 @@ const togglePermission = computed(() =>
background: var(--brand-d);
border-color: var(--brand-d);
}
@media (max-width: 680px) {
.ft-spacer {
display: none;
}
}
</style>