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

# Conflicts:
#	src/modules/container-loading/pages/GlobalSmartLoadingView.vue
#	src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue
#	src/modules/domestic-containers/pages/DomesticContainersView.vue
This commit is contained in:
2026-09-04 12:35:34 +08:00
24 changed files with 649 additions and 156 deletions
+16
View File
@@ -11,6 +11,7 @@ const ElDialogStub = defineComponent({
showClose: Boolean,
closeOnClickModal: Boolean,
closeOnPressEscape: Boolean,
fullscreen: Boolean,
},
template: '<section data-testid="dialog"><slot name="header" /><slot /><slot name="footer" /></section>',
})
@@ -65,10 +66,25 @@ describe('AppDialog', () => {
expect(wrapper.findAll('button')).toHaveLength(0)
})
it('provides opt-in fullscreen controls without changing ordinary dialogs', async () => {
const ordinary = mountDialog()
expect(ordinary.getComponent(ElDialogStub).props('fullscreen')).toBe(false)
expect(ordinary.find('button[aria-label="展开全屏"]').exists()).toBe(false)
const wrapper = mountDialog({ fullscreenable: true, defaultFullscreen: true })
expect(wrapper.getComponent(ElDialogStub).props('fullscreen')).toBe(true)
expect(wrapper.find('button[aria-label="退出全屏"]').exists()).toBe(true)
await wrapper.get('button[aria-label="退出全屏"]').trigger('click')
expect(wrapper.getComponent(ElDialogStub).props('fullscreen')).toBe(false)
expect(wrapper.find('button[aria-label="展开全屏"]').exists()).toBe(true)
})
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('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)')
})
+35 -2
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Maximize2, Minimize2 } from '@lucide/vue'
import { computed, ref, watch } from 'vue'
export type DialogConfirmType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
@@ -22,6 +23,8 @@ const props = withDefaults(defineProps<{
closeOnPressEscape?: boolean
destroyOnClose?: boolean
lockWhileSubmitting?: boolean
fullscreenable?: boolean
defaultFullscreen?: boolean
}>(), {
width: 'min(560px, calc(100vw - 28px))',
loading: false,
@@ -38,15 +41,26 @@ const props = withDefaults(defineProps<{
closeOnPressEscape: true,
destroyOnClose: false,
lockWhileSubmitting: true,
fullscreenable: false,
defaultFullscreen: false,
})
const open = defineModel<boolean>({ default: false })
const fullscreen = ref(props.defaultFullscreen)
const emit = defineEmits<{
confirm: []
cancel: []
}>()
const submittingLocked = computed(() => props.lockWhileSubmitting && props.confirmLoading)
watch(open, (value) => {
if (value) fullscreen.value = props.defaultFullscreen
})
function toggleFullscreen() {
fullscreen.value = !fullscreen.value
}
function closeDialog() {
if (submittingLocked.value) return
open.value = false
@@ -65,8 +79,23 @@ function closeDialog() {
:close-on-click-modal="closeOnClickModal && !submittingLocked"
:close-on-press-escape="closeOnPressEscape && !submittingLocked"
:destroy-on-close="destroyOnClose"
:fullscreen="fullscreenable && fullscreen"
>
<template v-if="$slots.header" #header><slot name="header" /></template>
<template v-if="$slots.header || fullscreenable" #header>
<div v-if="fullscreenable" 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>
</div>
<el-tooltip :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>
<div v-loading="loading" class="app-dialog__body" :aria-busy="loading || confirmLoading">
<slot />
</div>
@@ -89,8 +118,12 @@ function closeDialog() {
<style scoped>
:global(.app-dialog.el-dialog) { 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; }
.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__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; }