This commit is contained in:
zlwu committed 2026-05-21 11:25:43 +08:00
1 parent 74f751d0d1
commit ad0e849f6d
50 files changed
+1569 -659

No files matched your search

@@ -0,0 +1,510 @@
<!-- 弹窗详情/表单加载骨架(Fresh Mint,对齐 SystemViewDialog / dialog-form-layout) -->
<template>
<div
class="dialog-detail-skeleton"
:class="[
`dialog-detail-skeleton--${variant}`,
{ 'dialog-detail-skeleton--compact': compact }
]"
aria-busy="true"
aria-label="内容加载中"
>
<!-- SystemViewDialog 布局 -->
<template v-if="variant === 'system-view' || variant === 'system-view-image'">
<el-skeleton animated class="dds-hero">
<template #template>
<div class="dds-hero__inner">
<el-skeleton-item
:variant="variant === 'system-view-image' ? 'image' : 'rect'"
class="dds-hero__icon"
/>
<div class="dds-hero__main">
<el-skeleton-item variant="h1" class="dds-hero__title" />
<el-skeleton-item variant="text" class="dds-hero__meta" />
</div>
</div>
</template>
</el-skeleton>
<div
v-for="i in resolvedSectionCount"
:key="`sv-${i}`"
class="dds-section dds-section--view"
>
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="h3" class="dds-section__title" />
<div class="dds-kv-grid" :class="`dds-kv-grid--cols-${gridColumns}`">
<div v-for="f in resolvedFieldCount" :key="f" class="dds-kv-field">
<el-skeleton-item variant="text" class="dds-kv-field__label" />
<el-skeleton-item variant="p" class="dds-kv-field__value" />
</div>
</div>
</template>
</el-skeleton>
</div>
</template>
<!-- CRUD 表单弹窗(无 Tab) -->
<template v-else-if="variant === 'form'">
<div
v-for="i in resolvedSectionCount"
:key="`form-${i}`"
class="dds-section dds-section--form"
>
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="h3" class="dds-section__title dds-section__title--form" />
<div class="dds-form-grid" :class="`dds-form-grid--cols-${gridColumns}`">
<div v-for="f in resolvedFieldCount" :key="f" class="dds-form-field">
<el-skeleton-item variant="text" class="dds-form-field__label" />
<el-skeleton-item variant="rect" class="dds-form-field__control" />
</div>
</div>
</template>
</el-skeleton>
</div>
</template>
<!-- 带 Sticky Tab 的表单弹窗(协议单/捐赠单等) -->
<template v-else-if="variant === 'tabs-form'">
<div class="dds-tabs">
<el-skeleton-item
v-for="t in resolvedTabCount"
:key="t"
variant="rect"
class="dds-tabs__item"
/>
</div>
<div
v-for="i in resolvedSectionCount"
:key="`tf-${i}`"
class="dds-section dds-section--form"
>
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="h3" class="dds-section__title dds-section__title--form" />
<div class="dds-form-grid dds-form-grid--cols-2">
<div v-for="f in resolvedFieldCount" :key="f" class="dds-form-field">
<el-skeleton-item variant="text" class="dds-form-field__label" />
<el-skeleton-item variant="rect" class="dds-form-field__control" />
</div>
</div>
</template>
</el-skeleton>
</div>
</template>
<!-- 大宽查看弹窗(采集协议单等:KPI + 多区块) -->
<template v-else-if="variant === 'tabs-detail'">
<div class="dds-tabs">
<el-skeleton-item
v-for="t in resolvedTabCount"
:key="t"
variant="rect"
class="dds-tabs__item"
/>
</div>
<el-skeleton animated class="dds-kpi">
<template #template>
<div class="dds-kpi__grid">
<div v-for="i in 4" :key="i" class="dds-kpi__card">
<el-skeleton-item variant="circle" class="dds-kpi__icon" />
<div class="dds-kpi__text">
<el-skeleton-item variant="text" class="dds-kpi__label" />
<el-skeleton-item variant="h3" class="dds-kpi__value" />
</div>
</div>
</div>
</template>
</el-skeleton>
<div
v-for="(sec, sIdx) in tabsDetailSections"
:key="sec.key"
class="dds-section"
:class="{ 'dds-section--green': sec.tone === 'green' }"
>
<el-skeleton animated>
<template #template>
<el-skeleton-item variant="h3" class="dds-section__title" />
<div
v-if="sec.fieldCount > 0"
class="dds-data-grid"
:class="`dds-data-grid--${sec.cols}`"
>
<div v-for="f in sec.fieldCount" :key="f" class="dds-data-field">
<el-skeleton-item variant="text" class="dds-data-field__label" />
<el-skeleton-item variant="p" class="dds-data-field__value" />
</div>
</div>
<div v-if="sIdx === 1" class="dds-subcards">
<div v-for="j in 3" :key="j" class="dds-subcard">
<el-skeleton-item variant="text" style="width: 28%; margin-bottom: 12px" />
<div class="dds-data-grid dds-data-grid--4">
<div v-for="k in 4" :key="k" class="dds-data-field">
<el-skeleton-item variant="text" class="dds-data-field__label" />
<el-skeleton-item variant="p" class="dds-data-field__value" />
</div>
</div>
</div>
</div>
</template>
</el-skeleton>
</div>
</template>
<!-- 简易块(工作流/订单等自定义弹窗) -->
<template v-else>
<div
v-for="i in resolvedSectionCount"
:key="`simple-${i}`"
class="dds-section dds-section--simple"
>
<el-skeleton animated :rows="resolvedFieldCount" />
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
/** system-view | system-view-image | form | tabs-form | tabs-detail | simple */
variant?: 'system-view' | 'system-view-image' | 'form' | 'tabs-form' | 'tabs-detail' | 'simple'
sectionCount?: number
fieldCount?: number
tabCount?: number
gridColumns?: 2 | 3 | 4
compact?: boolean
}>(),
{
variant: 'system-view',
sectionCount: 2,
fieldCount: 6,
tabCount: 5,
gridColumns: 2,
compact: false
}
)
const resolvedSectionCount = computed(() => Math.max(1, props.sectionCount))
const resolvedFieldCount = computed(() => Math.max(2, props.fieldCount))
const resolvedTabCount = computed(() => Math.max(3, props.tabCount))
const tabsDetailSections = [
{ key: 'basic', tone: 'white' as const, cols: 4, fieldCount: 8 },
{ key: 'party', tone: 'green' as const, cols: 4, fieldCount: 0 },
{ key: 'collection', tone: 'white' as const, cols: 4, fieldCount: 8 },
{ key: 'sample', tone: 'white' as const, cols: 4, fieldCount: 6 },
{ key: 'payment', tone: 'white' as const, cols: 4, fieldCount: 8 }
]
</script>
<style scoped lang="scss">
.dialog-detail-skeleton {
display: flex;
flex-direction: column;
gap: 0;
min-height: min(360px, calc(100vh - 240px));
&--form,
&--tabs-form {
gap: 16px;
padding: 2px 0 8px;
min-height: min(320px, calc(100vh - 240px));
}
&--tabs-detail {
gap: 20px;
min-height: min(640px, calc(100vh - 240px));
}
&--compact {
min-height: min(280px, calc(100vh - 200px));
}
}
.dds-hero {
:deep(.el-skeleton) {
width: 100%;
}
}
.dds-hero__inner {
display: flex;
gap: 16px;
padding: 20px 22px;
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 55%);
border-bottom: 1px solid #d9e8e2;
align-items: center;
}
.dds-hero__icon {
width: 72px;
height: 72px;
border-radius: 12px;
flex-shrink: 0;
}
.dds-hero__main {
flex: 1;
min-width: 0;
}
.dds-hero__title {
width: 42%;
height: 22px;
margin-bottom: 10px;
}
.dds-hero__meta {
width: 58%;
height: 14px;
}
.dialog-detail-skeleton--compact .dds-hero__inner {
padding: 12px 18px;
gap: 12px;
}
.dialog-detail-skeleton--compact .dds-hero__icon {
width: 48px;
height: 48px;
}
.dds-section--view {
padding: 18px 22px;
border-bottom: 1px solid #ebeef5;
&:last-child {
border-bottom: none;
}
:deep(.el-skeleton) {
width: 100%;
}
}
.dds-section--form,
.dds-section--simple {
padding: 4px 0;
:deep(.el-skeleton) {
width: 100%;
}
}
.dds-section--green {
padding: 22px 24px;
background: #f0fdf4;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.dds-section:not(.dds-section--view):not(.dds-section--green) {
&.dds-section--form {
padding: 8px 0;
}
}
.dds-section__title {
width: 96px;
height: 18px;
margin-bottom: 14px;
&--form {
width: 80px;
margin-bottom: 16px;
}
}
.dds-kv-grid {
display: grid;
gap: 12px 20px;
grid-template-columns: repeat(2, minmax(0, 1fr));
&--cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.dds-kv-field {
display: grid;
grid-template-columns: 96px 1fr;
gap: 8px 12px;
}
.dds-kv-field__label {
width: 80%;
height: 13px;
}
.dds-kv-field__value {
width: 92%;
height: 14px;
}
.dds-form-grid {
display: grid;
gap: 14px 20px;
grid-template-columns: repeat(2, minmax(0, 1fr));
&--cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.dds-form-field__label {
width: 40%;
height: 12px;
margin-bottom: 8px;
}
.dds-form-field__control {
width: 100%;
height: 36px;
border-radius: 8px;
}
.dds-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 4px;
background: #f0fdf4;
border-radius: 8px;
width: fit-content;
max-width: 100%;
margin-bottom: 4px;
}
.dds-tabs__item {
width: 88px;
height: 36px;
border-radius: 6px;
}
.dds-kpi {
margin-bottom: 4px;
:deep(.el-skeleton) {
width: 100%;
}
}
.dds-kpi__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
padding: 6px;
background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 52%, #ecfdf5 100%);
border: 1px solid #a7f3d0;
border-radius: 12px;
}
.dds-kpi__card {
display: flex;
gap: 12px;
padding: 14px 18px;
&:not(:last-child) {
border-right: 1px solid rgba(167, 243, 208, 0.75);
}
}
.dds-kpi__icon {
width: 40px;
height: 40px;
flex-shrink: 0;
}
.dds-kpi__text {
flex: 1;
min-width: 0;
}
.dds-kpi__label {
width: 48%;
height: 11px;
}
.dds-kpi__value {
width: 72%;
height: 16px;
margin-top: 8px;
}
.dds-section:not(.dds-section--view) {
border-radius: 12px;
padding: 22px 24px;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
:deep(.el-skeleton) {
width: 100%;
}
}
.dds-data-grid {
display: grid;
gap: 14px 20px;
&--4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
.dds-data-field__label {
width: 56%;
height: 12px;
margin-bottom: 6px;
}
.dds-data-field__value {
width: 88%;
height: 14px;
}
.dds-subcards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-top: 4px;
}
.dds-subcard {
padding: 16px;
background: #fff;
border-radius: 10px;
border: 1px solid #e2e8f0;
}
@media (max-width: 1200px) {
.dds-kpi__grid {
grid-template-columns: repeat(2, 1fr);
}
.dds-kpi__card {
border-right: none !important;
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
&:nth-last-child(-n + 2) {
border-bottom: none;
}
}
.dds-subcards {
grid-template-columns: 1fr;
}
.dds-data-grid--4 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 900px) {
.dds-kv-grid--cols-3 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>
@@ -0,0 +1,51 @@
<!-- 弹窗正文:加载中显示骨架,避免 dialog-form-layout 先小后大 -->
<template>
<div
class="dialog-form-body"
:class="[bodyClass, { 'is-dialog-loading': loading }]"
>
<DialogDetailSkeleton
v-if="loading"
:variant="skeletonVariant"
:section-count="skeletonSectionCount"
:field-count="skeletonFieldCount"
:tab-count="skeletonTabCount"
:grid-columns="skeletonGridColumns"
:compact="skeletonCompact"
/>
<slot v-else />
</div>
</template>
<script setup lang="ts">
import DialogDetailSkeleton from '@/components/business/dialog-detail-skeleton/index.vue'
withDefaults(
defineProps<{
loading?: boolean
bodyClass?: string
skeletonVariant?: 'system-view' | 'system-view-image' | 'form' | 'tabs-form' | 'tabs-detail' | 'simple'
skeletonSectionCount?: number
skeletonFieldCount?: number
skeletonTabCount?: number
skeletonGridColumns?: 2 | 3 | 4
skeletonCompact?: boolean
}>(),
{
loading: false,
bodyClass: '',
skeletonVariant: 'form',
skeletonSectionCount: 2,
skeletonFieldCount: 6,
skeletonTabCount: 5,
skeletonGridColumns: 2,
skeletonCompact: false
}
)
</script>
<style scoped lang="scss">
.dialog-form-body.is-dialog-loading {
min-height: min(400px, calc(100vh - 220px));
}
</style>
@@ -9,7 +9,14 @@
@update:model-value="emit('update:modelValue', $event)"
@closed="handleClosed"
>
<div v-loading="loading" class="product-form-container">
<div class="product-form-container" :class="{ 'is-view-loading': loading }">
<DialogDetailSkeleton
v-if="loading"
variant="system-view-image"
:section-count="3"
:grid-columns="2"
/>
<template v-else>
<div class="view-hero">
<el-image
v-if="formData.imageUrl"
@@ -229,6 +236,7 @@
</div>
</div>
</div>
</template>
</div>
<template #footer>
@@ -261,6 +269,7 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { Close, Document, Money, Picture, Setting, VideoPlay } from '@element-plus/icons-vue'
import DialogDetailSkeleton from '@/components/business/dialog-detail-skeleton/index.vue'
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
import {
fetchProductDetail,
@@ -616,6 +625,11 @@ watch(
.product-form-container {
max-height: 62vh;
overflow-y: auto;
&.is-view-loading {
min-height: min(420px, calc(100vh - 220px));
overflow: hidden;
}
padding-right: 6px;
&::-webkit-scrollbar {
width: 6px;
@@ -9,6 +9,9 @@
:icon="Tickets"
:meta="viewDetail?.taskNo || ''"
:sections="viewSections"
:loading="loading"
:skeleton-section-count="3"
:skeleton-field-count="8"
append-to-body
compact
modal-class="qc-task-view-dialog-mask"
@@ -13,7 +13,16 @@
:modal-class="modalClass"
style="backdrop-filter: none"
>
<div class="dialog-scroll">
<div class="dialog-scroll" :class="{ 'is-view-loading': loading }">
<DialogDetailSkeleton
v-if="loading"
variant="system-view"
:section-count="skeletonSectionCount"
:field-count="skeletonFieldCount"
:grid-columns="gridColumns"
:compact="compact"
/>
<template v-else>
<!-- hero -->
<div class="view-hero">
<div class="view-hero-icon">
@@ -64,6 +73,7 @@
</div>
</div>
<slot name="append" />
</template>
</div>
<template #footer>
@@ -77,6 +87,7 @@
<script setup lang="ts">
import { computed, type Component } from 'vue'
import { Close, Document } from '@element-plus/icons-vue'
import DialogDetailSkeleton from '@/components/business/dialog-detail-skeleton/index.vue'
function isIconComponent(val: unknown): val is Component {
return val != null && (typeof val === 'object' || typeof val === 'function')
@@ -129,6 +140,12 @@
dialogClass?: string
/** 紧凑布局:缩小头部/区块间距与内容区最大高度 */
compact?: boolean
/** 详情异步加载中:展示骨架屏 */
loading?: boolean
/** 骨架区块数量(默认 2) */
skeletonSectionCount?: number
/** 每区块骨架字段行数(默认 6) */
skeletonFieldCount?: number
}>(),
{
width: '720px',
@@ -136,7 +153,10 @@
appendToBody: false,
modalClass: '',
dialogClass: '',
compact: false
compact: false,
loading: false,
skeletonSectionCount: 2,
skeletonFieldCount: 6
}
)
@@ -166,6 +186,10 @@
.dialog-scroll {
overflow-y: auto;
max-height: calc(100vh - 120px);
&.is-view-loading {
overflow: hidden;
}
}
.view-hero {