Merge remote-tracking branch 'origin/release/test' into dev

# Conflicts:
#	AGENTS.md
#	src/modules/container-loading/pages/GlobalSmartLoadingView.vue
#	src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue
This commit is contained in:
2026-09-04 16:21:32 +08:00
26 changed files with 278 additions and 233 deletions
+1
View File
@@ -18,6 +18,7 @@
- XLSX 批量导入使用 `BatchImportDialog.vue`;页面只提供模板下载、导入调用和成功后刷新,不复制文件校验、上传状态、错误报告或弹窗 CSS。
- 模态框和抽屉优先使用 `AppDialog.vue`、`AppDrawer.vue`;但独立页面、弹框或抽屉的选择必须服从对应原型,不因已有组件方便而替换承载方式。
- `AppDialog` 未提供自定义 `#header` 时保留 Element 原生 `title` 标题栏;`fullscreenable` 只在原标题栏追加全屏控件,不得仅为标题或全屏按钮再造一层 header。只有原型确有复合标题结构时才使用 `#header`。
- 加载失败组合 `LoadErrorState.vue` 与 `ApiErrorMeta.vue`,保留重试和 `requestId`/`traceId`;状态与复制优先使用 `StatusBadge.vue`、`CopyButton.vue`。
- 参考页面:查询和表格见 `../../modules/master-data/express-companies/pages/ExpressCompaniesView.vue`,批量导入见该页面与 `../../modules/master-data/counterparties/pages/PartnersView.vue`,抽屉见 `../../modules/downloads/pages/DownloadCenterView.vue`。
+15 -2
View File
@@ -8,12 +8,13 @@ import AppDialog from './AppDialog.vue'
const ElDialogStub = defineComponent({
props: {
modelValue: Boolean,
title: String,
showClose: Boolean,
closeOnClickModal: Boolean,
closeOnPressEscape: Boolean,
fullscreen: Boolean,
},
template: '<section data-testid="dialog"><slot name="header" /><slot /><slot name="footer" /></section>',
template: '<section data-testid="dialog"><h2 v-if="!$slots.header" data-testid="native-title">{{ title }}</h2><slot name="header" /><slot /><slot name="footer" /></section>',
})
const ElButtonStub = defineComponent({
@@ -73,6 +74,9 @@ describe('AppDialog', () => {
const wrapper = mountDialog({ fullscreenable: true, defaultFullscreen: true })
expect(wrapper.getComponent(ElDialogStub).props('fullscreen')).toBe(true)
expect(wrapper.get('[data-testid="native-title"]').text()).toBe('编辑资料')
expect(wrapper.find('.app-dialog__header').exists()).toBe(false)
expect(wrapper.find('.app-dialog__native-fullscreen-control').exists()).toBe(true)
expect(wrapper.find('button[aria-label="退出全屏"]').exists()).toBe(true)
await wrapper.get('button[aria-label="退出全屏"]').trigger('click')
@@ -83,9 +87,18 @@ describe('AppDialog', () => {
it('keeps the dialog chrome fixed while long content scrolls in the body', () => {
const source = readFileSync(resolve(process.cwd(), 'src/shared/components/AppDialog.vue'), 'utf8')
expect(source).toContain(':global(.app-dialog.el-dialog) { display: flex;')
expect(source).toContain(':global(.app-dialog.el-dialog) { position: relative; display: flex;')
expect(source).toContain('width: 100%; max-width: 100%; height: 100vh;')
expect(source).toContain(':global(.app-dialog .el-dialog__body) { min-height: 0; overflow-y: auto;')
expect(source).toContain('max-height: calc(100svh - 20px)')
})
it('keeps the native title bar when fullscreen is enabled without a custom header', () => {
const source = readFileSync(resolve(process.cwd(), 'src/shared/components/AppDialog.vue'), 'utf8')
expect(source).toContain('<template v-if="$slots.header" #header>')
expect(source).not.toContain('<template v-if="$slots.header || fullscreenable" #header>')
expect(source).toContain("fullscreenable && !$slots.header")
expect(source).toContain('app-dialog__native-fullscreen-control')
})
})
+21 -7
View File
@@ -80,22 +80,33 @@ function closeDialog() {
:close-on-press-escape="closeOnPressEscape && !submittingLocked"
:destroy-on-close="destroyOnClose"
:fullscreen="fullscreenable && fullscreen"
:class="{ 'has-native-fullscreen-control': fullscreenable && !$slots.header }"
>
<template v-if="$slots.header || fullscreenable" #header>
<div v-if="fullscreenable" class="app-dialog__header">
<template v-if="$slots.header" #header>
<div class="app-dialog__header">
<div class="app-dialog__header-content">
<slot v-if="$slots.header" name="header" :fullscreen="fullscreen" :toggle-fullscreen="toggleFullscreen" />
<span v-else>{{ title }}</span>
<slot name="header" :fullscreen="fullscreen" :toggle-fullscreen="toggleFullscreen" />
</div>
<el-tooltip :content="fullscreen ? '退出全屏' : '展开全屏'" placement="bottom">
<el-tooltip v-if="fullscreenable" :content="fullscreen ? '退出全屏' : '展开全屏'" placement="bottom">
<el-button text :aria-label="fullscreen ? '退出全屏' : '展开全屏'" @click="toggleFullscreen">
<Minimize2 v-if="fullscreen" :size="18" />
<Maximize2 v-else :size="18" />
</el-button>
</el-tooltip>
</div>
<slot v-else name="header" />
</template>
<el-tooltip v-if="fullscreenable && !$slots.header" :content="fullscreen ? '退出全屏' : '展开全屏'" placement="bottom">
<el-button
class="app-dialog__native-fullscreen-control"
:class="{ 'is-without-close': !showClose || submittingLocked }"
text
:aria-label="fullscreen ? '退出全屏' : '展开全屏'"
@click="toggleFullscreen"
>
<Minimize2 v-if="fullscreen" :size="18" />
<Maximize2 v-else :size="18" />
</el-button>
</el-tooltip>
<div v-loading="loading" class="app-dialog__body" :aria-busy="loading || confirmLoading">
<slot />
</div>
@@ -117,13 +128,16 @@ function closeDialog() {
</template>
<style scoped>
:global(.app-dialog.el-dialog) { display: flex; max-height: min(90vh, 900px); flex-direction: column; overflow: hidden; }
:global(.app-dialog.el-dialog) { position: relative; display: flex; max-height: min(90vh, 900px); flex-direction: column; overflow: hidden; }
:global(.app-dialog.el-dialog.is-fullscreen) { width: 100%; max-width: 100%; height: 100vh; max-height: 100vh; margin: 0 !important; border-radius: 0; }
:global(.app-dialog .el-dialog__header), :global(.app-dialog .el-dialog__footer) { flex: 0 0 auto; }
:global(.app-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
:global(.app-dialog.has-native-fullscreen-control .el-dialog__title) { display: block; padding-right: 44px; }
.app-dialog__header { display: flex; width: 100%; min-width: 0; align-items: center; justify-content: space-between; gap: 12px; color: var(--cc-text); font-size: 18px; font-weight: 600; line-height: 24px; }
.app-dialog__header-content { min-width: 0; flex: 1; }
.app-dialog__header > :deep(.el-button) { flex: 0 0 auto; margin-right: 24px; color: var(--cc-muted); }
.app-dialog__native-fullscreen-control { position: absolute; z-index: 2; top: 10px; right: 48px; color: var(--cc-muted); }
.app-dialog__native-fullscreen-control.is-without-close { right: 12px; }
.app-dialog__body { min-height: 0; }
.app-dialog__footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
.app-dialog__footer :deep(.el-button) { margin-left: 0; }