task-248(admin.html观感对齐): 模态/抽屉容器统一(ModalShell/DrawerShell 收口尺寸与关闭语义)

This commit is contained in:
2026-09-05 21:01:23 +08:00
parent f7ad99deda
commit 4e86506f3e
4 changed files with 188 additions and 0 deletions
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { computed } from 'vue'
import { dialogTitleOf, drawerLayoutSpec } from './modal-model'
/**
* DrawerShell 右侧抽屉容器(module 13 task 248):封装 el-drawer,统一 admin 尺寸
* 与关闭语义(遮罩/Esc),标题透传默认头部、footer 槽承接底部按钮,构成头/体/脚三段。
*/
const props = withDefaults(
defineProps<{ modelValue: boolean; title?: string; subtitle?: string; width?: string | number }>(),
{ title: '', subtitle: '', width: drawerLayoutSpec().width },
)
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
const visible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit('update:modelValue', v),
})
const spec = computed(() => drawerLayoutSpec())
const head = computed(() => dialogTitleOf({ title: props.title, subtitle: props.subtitle }))
</script>
<template>
<el-drawer
v-model="visible"
:size="width"
:title="head.title"
:close-on-click-modal="spec.maskClosable"
:close-on-press-escape="spec.escClosable"
class="admin-drawer-shell"
>
<p v-if="head.subtitle" class="admin-drawer-subtitle">{{ head.subtitle }}</p>
<slot />
<template #footer>
<slot name="footer" />
</template>
</el-drawer>
</template>
<style scoped>
.admin-drawer-subtitle { margin: 0 0 12px; color: var(--admin-muted); font-size: 13px; }
</style>
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { computed } from 'vue'
import { dialogTitleOf, modalLayoutSpec } from './modal-model'
/**
* ModalShell 居中模态容器(module 13 task 248):封装 el-dialog,统一 admin 尺寸/圆角
* 与关闭语义,标题透传给 Element 默认头部;body 用默认槽、操作区走 footer 槽。
*/
const props = withDefaults(
defineProps<{ modelValue: boolean; title?: string; subtitle?: string; width?: string | number }>(),
{ title: '', subtitle: '', width: modalLayoutSpec().width },
)
const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
const visible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit('update:modelValue', v),
})
const spec = computed(() => modalLayoutSpec())
const head = computed(() => dialogTitleOf({ title: props.title, subtitle: props.subtitle }))
</script>
<template>
<el-dialog
v-model="visible"
:width="width"
:title="head.title"
:close-on-click-modal="spec.maskClosable"
:close-on-press-escape="spec.escClosable"
class="admin-modal-shell"
>
<p v-if="head.subtitle" class="admin-modal-subtitle">{{ head.subtitle }}</p>
<slot />
<template #footer>
<slot name="footer" />
</template>
</el-dialog>
</template>
<style scoped>
.admin-modal-subtitle { margin: 0 0 12px; color: var(--admin-muted); font-size: 13px; }
</style>
@@ -0,0 +1,30 @@
/**
* 模态/抽屉布局模型(module 13 task 248):统一两种承载容器的尺寸与关闭语义,
* 对齐 admin.html —— 居中 modal width min(920px,100vw-48)、radius14;右侧 drawer
* width min(460px,100%);遮罩点击与 Esc 均关闭。页面接入统一尺寸与头/脚槽。
*/
export const ADMIN_MODAL_WIDTH = 'min(920px, 100vw - 48px)'
export const ADMIN_DRAWER_WIDTH = 'min(460px, 100%)'
export const ADMIN_MODAL_RADIUS = 14
export interface AdminContainerSpec {
width: string
radius?: number
maskClosable: boolean
escClosable: boolean
}
export function modalLayoutSpec(): AdminContainerSpec {
return { width: ADMIN_MODAL_WIDTH, radius: ADMIN_MODAL_RADIUS, maskClosable: true, escClosable: true }
}
export function drawerLayoutSpec(): AdminContainerSpec {
return { width: ADMIN_DRAWER_WIDTH, maskClosable: true, escClosable: true }
}
/** 页面头标题归一:空/空白标题兜底「操作」,副标题去空白后无则省略。 */
export function dialogTitleOf(opts: { title?: string; subtitle?: string }): { title: string; subtitle?: string } {
const title = opts.title?.trim()
const subtitle = opts.subtitle?.trim()
return { title: title || '操作', ...(subtitle ? { subtitle } : {}) }
}
+74
View File
@@ -0,0 +1,74 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import {
ADMIN_DRAWER_WIDTH,
ADMIN_MODAL_RADIUS,
ADMIN_MODAL_WIDTH,
dialogTitleOf,
drawerLayoutSpec,
modalLayoutSpec,
} from '../src/components/modal-model.ts'
// module 13 task 248:模态/抽屉容器统一 —— 居中 modal min(920px,100vw-48)、右侧 drawer
// min(460px,100%)、三段式头/体/脚与关闭语义对齐 admin,页面接入统一宽度/结构。
test('test_task_248_container_model_normal_primary_path', () => {
const modal = modalLayoutSpec()
assert.equal(modal.width, ADMIN_MODAL_WIDTH)
assert.equal(modal.radius, ADMIN_MODAL_RADIUS)
assert.equal(ADMIN_MODAL_RADIUS, 14)
const drawer = drawerLayoutSpec()
assert.equal(drawer.width, ADMIN_DRAWER_WIDTH)
})
test('test_task_248_container_model_normal_variant_input', () => {
// 正常变体:modal/drawer 都支持遮罩点击与 Esc 关闭(对齐 admin 语义)。
assert.equal(modalLayoutSpec().maskClosable, true)
assert.equal(modalLayoutSpec().escClosable, true)
assert.equal(drawerLayoutSpec().maskClosable, true)
assert.equal(drawerLayoutSpec().escClosable, true)
})
test('test_task_248_container_model_normal_repeated_operation_is_idempotent', () => {
assert.deepEqual(modalLayoutSpec(), modalLayoutSpec())
assert.deepEqual(drawerLayoutSpec(), drawerLayoutSpec())
})
test('test_task_248_container_model_boundary_empty_input', () => {
// 边界空:无标题时页头兜底「操作」;空白标题同样兜底。
assert.equal(dialogTitleOf({}).title, '操作')
assert.equal(dialogTitleOf({ title: ' ' }).title, '操作')
})
test('test_task_248_container_model_boundary_single_item', () => {
// 边界单元素:带副标题保留、去首尾空白。
const head = dialogTitleOf({ title: ' 编辑店铺 ', subtitle: ' 店铺分组/账号凭证明细 ' })
assert.equal(head.title, '编辑店铺')
assert.equal(head.subtitle, '店铺分组/账号凭证明细')
})
test('test_task_248_container_model_boundary_limit_or_missing_field', () => {
// 边界上限:宽度表达式满足 admin 尺寸口径。
assert.match(ADMIN_MODAL_WIDTH, /920px/)
assert.match(ADMIN_MODAL_WIDTH, /100vw/)
assert.match(ADMIN_DRAWER_WIDTH, /460px/)
})
test('test_task_248_modal_shell_dependency_failure_returns_actionable_message', () => {
// 依赖失败:ModalShell 真实封装 el-dialog,宽度来自模型,标题透传、带 footer 槽。
const modal = readSource('src/components/ModalShell.vue')
assert.match(modal, /el-dialog/)
assert.match(modal, /modalLayoutSpec|ADMIN_MODAL_WIDTH/)
assert.match(modal, /#footer/)
assert.ok(readSource('src/components/DrawerShell.vue').length > 0)
})
test('test_task_248_drawer_shell_dependency_failure_returns_actionable_message', () => {
const drawer = readSource('src/components/DrawerShell.vue')
assert.match(drawer, /el-drawer/)
assert.match(drawer, /drawerLayoutSpec|ADMIN_DRAWER_WIDTH/)
assert.match(drawer, /:title=|head\.title/, '标题透传到 drawer 默认头部')
assert.match(drawer, /#footer/, '提供 footer 槽以构成头/体/脚三段')
assert.match(drawer, /close-on-press-escape|close-on-click-modal/, '需配置 Esc/遮罩关闭语义')
})