调整UI布局
This commit is contained in:
parent
82217c0d35
commit
4c42260a29
@ -80,20 +80,21 @@ const isGenerating = computed(() =>
|
||||
|
||||
const canAct = computed(() => {
|
||||
if (isGenerating.value) return false
|
||||
if (props.actionState === 'generate-poster' && !props.canGenerate) return false
|
||||
return props.actionState !== 'generating'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.poster-action-bar {
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
min-height: 64px;
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-top: 1px solid var(--poster-border);
|
||||
background: #fff;
|
||||
background: rgb(255 255 255 / 97%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -119,15 +120,15 @@ const canAct = computed(() => {
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dot.done {
|
||||
background: #16a34a;
|
||||
background: var(--poster-accent);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@ -137,13 +138,45 @@ const canAct = computed(() => {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-buttons :deep(.el-button) {
|
||||
min-height: 38px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.action-buttons :deep(.el-button--primary) {
|
||||
min-width: 112px;
|
||||
--el-button-bg-color: var(--poster-accent);
|
||||
--el-button-border-color: var(--poster-accent);
|
||||
--el-button-hover-bg-color: var(--poster-accent-hover);
|
||||
--el-button-hover-border-color: var(--poster-accent-hover);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.status-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.poster-action-bar {
|
||||
padding: 0 12px;
|
||||
min-height: 68px;
|
||||
padding: 8px 12px max(8px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.progress-dots {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.action-buttons :deep(.el-button--primary) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<aside class="config-rail">
|
||||
<div class="rail-scroll">
|
||||
<PosterProductPanel
|
||||
ref="productPanelRef"
|
||||
:product-id="draft.productId"
|
||||
:product-source-type="draft.productSourceType"
|
||||
:product-source-id="draft.productSourceId"
|
||||
@ -13,6 +14,7 @@
|
||||
<el-divider class="panel-divider" />
|
||||
|
||||
<PosterSourcePanel
|
||||
ref="sourcePanelRef"
|
||||
:product-id="draft.productId"
|
||||
:product-source-type="draft.productSourceType"
|
||||
:product-source-id="draft.productSourceId"
|
||||
@ -32,6 +34,7 @@
|
||||
<el-divider class="panel-divider" />
|
||||
|
||||
<PosterCreativePanel
|
||||
ref="creativePanelRef"
|
||||
:scenario="draft.scenario"
|
||||
:output-mode="draft.outputMode"
|
||||
:export-size="draft.exportSize"
|
||||
@ -54,12 +57,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { PosterDraft } from '@/composables/usePosterWorkspace'
|
||||
import PosterProductPanel from './PosterProductPanel.vue'
|
||||
import PosterSourcePanel from './PosterSourcePanel.vue'
|
||||
import PosterCreativePanel from './PosterCreativePanel.vue'
|
||||
import PosterReferencePanel from './PosterReferencePanel.vue'
|
||||
|
||||
const productPanelRef = ref<InstanceType<typeof PosterProductPanel> | null>(null)
|
||||
const sourcePanelRef = ref<InstanceType<typeof PosterSourcePanel> | null>(null)
|
||||
const creativePanelRef = ref<InstanceType<typeof PosterCreativePanel> | null>(null)
|
||||
|
||||
const props = defineProps<{ draft: PosterDraft }>()
|
||||
const emit = defineEmits<{ 'update:draft': [patch: Partial<PosterDraft>] }>()
|
||||
|
||||
@ -96,6 +104,13 @@ function onProductSourceUpdate(source: {
|
||||
} : {}),
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openProductSelector: () => productPanelRef.value?.openSelector(),
|
||||
openSourceUpload: () => sourcePanelRef.value?.openUpload(),
|
||||
openSourceReview: () => sourcePanelRef.value?.openReview(),
|
||||
openTemplates: () => creativePanelRef.value?.openTemplates(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -103,22 +118,25 @@ function onProductSourceUpdate(source: {
|
||||
width: 300px;
|
||||
min-width: 280px;
|
||||
max-width: 320px;
|
||||
border-right: 1px solid var(--poster-border);
|
||||
background: #fafbfc;
|
||||
border: 1px solid var(--poster-border);
|
||||
border-radius: var(--poster-radius);
|
||||
background: var(--poster-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--poster-shadow);
|
||||
}
|
||||
|
||||
.rail-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.panel-divider {
|
||||
margin: 12px 0;
|
||||
margin: 14px 0;
|
||||
border-color: #edf1ee;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="creative-panel">
|
||||
<div ref="panelRoot" class="creative-panel">
|
||||
<div class="panel-header" @click="expanded = !expanded">
|
||||
<div class="header-left">
|
||||
<el-icon :size="16"><Brush /></el-icon>
|
||||
@ -200,6 +200,15 @@ const templates = ref<any[]>([])
|
||||
const showCustom = ref(false)
|
||||
const customW = ref(1024)
|
||||
const customH = ref(1792)
|
||||
const panelRoot = ref<HTMLElement | null>(null)
|
||||
|
||||
function openTemplates() {
|
||||
expanded.value = true
|
||||
panelRoot.value?.scrollIntoView({ block: 'start' })
|
||||
if (templates.value.length > 6) drawerVisible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ openTemplates })
|
||||
|
||||
// ── 尺寸预设 ──────────────────────────
|
||||
// ── 尺寸预设(根据输出模式切换)──────────
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
@update:copy-content="$emit('update:draft', { copyContent: $event })"
|
||||
@update:ai-raw-content="$emit('update:draft', { aiRawContent: $event })"
|
||||
@update:compliance="$emit('update:draft', {
|
||||
complianceStatus: $event.status,
|
||||
complianceStatus: normalizeComplianceStatus($event.status),
|
||||
complianceIssues: $event.issues,
|
||||
complianceRevision: $event.revision,
|
||||
})"
|
||||
@ -52,6 +52,17 @@ defineProps<{ draft: PosterDraft }>()
|
||||
defineEmits<{ 'update:draft': [patch: Partial<PosterDraft>] }>()
|
||||
|
||||
const activeTab = ref('copy')
|
||||
|
||||
function openTab(tab: 'copy' | 'layout' | 'delivery') {
|
||||
activeTab.value = tab
|
||||
}
|
||||
|
||||
defineExpose({ openTab })
|
||||
|
||||
function normalizeComplianceStatus(status: string): PosterDraft['complianceStatus'] {
|
||||
if (status === 'pass' || status === 'warn' || status === 'block') return status
|
||||
return 'unchecked'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -59,11 +70,13 @@ const activeTab = ref('copy')
|
||||
width: 320px;
|
||||
min-width: 300px;
|
||||
max-width: 360px;
|
||||
border-left: 1px solid var(--poster-border);
|
||||
background: #fff;
|
||||
border: 1px solid var(--poster-border);
|
||||
border-radius: var(--poster-radius);
|
||||
background: var(--poster-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--poster-shadow);
|
||||
}
|
||||
|
||||
.inspector-tabs {
|
||||
@ -76,14 +89,23 @@ const activeTab = ref('copy')
|
||||
.inspector-tabs :deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
background: #fafbfc;
|
||||
background: #f8faf8;
|
||||
border-bottom: 1px solid var(--poster-border);
|
||||
}
|
||||
|
||||
.inspector-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.inspector-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: var(--poster-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inspector-tabs :deep(.el-tabs__active-bar) {
|
||||
background-color: var(--poster-accent);
|
||||
}
|
||||
|
||||
.inspector-tabs :deep(.el-tab-pane) {
|
||||
|
||||
@ -161,6 +161,13 @@ const materialDialogVisible = ref(false)
|
||||
const editingMaterial = ref<any | null>(null)
|
||||
const manualUploadEnabled = ref(true)
|
||||
|
||||
function openSelector() {
|
||||
expanded.value = true
|
||||
drawerVisible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ openSelector })
|
||||
|
||||
const typeMap: Record<string, string> = {
|
||||
savings: '储蓄',
|
||||
ci: '重疾',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="source-panel">
|
||||
<div ref="panelRoot" class="source-panel">
|
||||
<div class="panel-header" @click="expanded = !expanded">
|
||||
<div class="header-left">
|
||||
<el-icon :size="16"><Document /></el-icon>
|
||||
@ -166,6 +166,20 @@ const expanded = ref(true)
|
||||
const editingFields = ref(false)
|
||||
const confirming = ref(false)
|
||||
const localPassword = ref('')
|
||||
const panelRoot = ref<HTMLElement | null>(null)
|
||||
|
||||
function openUpload() {
|
||||
expanded.value = true
|
||||
panelRoot.value?.scrollIntoView({ block: 'start' })
|
||||
}
|
||||
|
||||
function openReview() {
|
||||
expanded.value = true
|
||||
editingFields.value = true
|
||||
panelRoot.value?.scrollIntoView({ block: 'start' })
|
||||
}
|
||||
|
||||
defineExpose({ openUpload, openReview })
|
||||
|
||||
const localFields = ref({
|
||||
age: null, gender: null, currency: null, sum_assured: null,
|
||||
|
||||
@ -118,7 +118,14 @@ const canvasStyle = computed(() => {
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #f5f6f8;
|
||||
border: 1px solid var(--poster-border);
|
||||
border-radius: var(--poster-radius);
|
||||
background:
|
||||
linear-gradient(90deg, rgb(23 107 77 / 3%) 1px, transparent 1px),
|
||||
linear-gradient(rgb(23 107 77 / 3%) 1px, transparent 1px),
|
||||
#edf2ef;
|
||||
background-size: 24px 24px;
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 80%);
|
||||
}
|
||||
|
||||
.stage-toolbar {
|
||||
@ -126,18 +133,34 @@ const canvasStyle = computed(() => {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 10;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--poster-border);
|
||||
border-radius: 10px;
|
||||
background: rgb(255 255 255 / 92%);
|
||||
box-shadow: 0 8px 22px rgb(24 61 44 / 10%);
|
||||
}
|
||||
|
||||
.stage-canvas {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
padding: 32px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.stage-canvas {
|
||||
padding: 18px 10px 24px;
|
||||
}
|
||||
|
||||
.stage-toolbar {
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 加载状态 ──────────────────────── */
|
||||
.canvas-loading {
|
||||
position: absolute;
|
||||
@ -200,3 +223,8 @@ const canvasStyle = computed(() => {
|
||||
box-shadow: 0 4px 16px rgb(15 23 42 / 12%);
|
||||
}
|
||||
</style>
|
||||
padding: 4px;
|
||||
border: 1px solid var(--poster-border);
|
||||
border-radius: 10px;
|
||||
background: rgb(255 255 255 / 92%);
|
||||
box-shadow: 0 8px 22px rgb(24 61 44 / 10%);
|
||||
|
||||
@ -78,14 +78,15 @@ function onCommand(cmd: string) {
|
||||
|
||||
<style scoped>
|
||||
.poster-toolbar {
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
height: 68px;
|
||||
min-height: 68px;
|
||||
padding: 0 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
border-bottom: 1px solid var(--poster-border);
|
||||
background: #fff;
|
||||
background: rgb(255 255 255 / 97%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -102,8 +103,9 @@ function onCommand(cmd: string) {
|
||||
}
|
||||
|
||||
.title-input :deep(.el-input__inner) {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--poster-text);
|
||||
border: none;
|
||||
padding: 0;
|
||||
@ -113,12 +115,12 @@ function onCommand(cmd: string) {
|
||||
.title-input :deep(.el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.title-input :deep(.el-input__wrapper:hover),
|
||||
.title-input :deep(.el-input__wrapper:focus-within) {
|
||||
background: #f5f7fa;
|
||||
background: var(--poster-accent-light);
|
||||
}
|
||||
|
||||
.save-indicator {
|
||||
@ -152,4 +154,40 @@ function onCommand(cmd: string) {
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-right :deep(.el-button) {
|
||||
min-height: 36px;
|
||||
border-radius: 10px;
|
||||
color: var(--poster-muted);
|
||||
}
|
||||
|
||||
.toolbar-right :deep(.el-button:hover) {
|
||||
color: var(--poster-accent);
|
||||
background: var(--poster-accent-light);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.poster-toolbar {
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
max-width: min(48vw, 220px);
|
||||
}
|
||||
|
||||
.title-input :deep(.el-input__inner) {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.toolbar-right :deep(.el-button span) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar-right :deep(.el-button) {
|
||||
width: 36px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="poster-workspace" :class="{ 'mobile-mode': isMobile }">
|
||||
<div ref="workspaceRef" class="poster-workspace" :class="{ 'mobile-mode': isMobile }">
|
||||
<!-- 顶部工具栏 -->
|
||||
<PosterToolbar
|
||||
:title="posterTitle || ws.draft.value.productName || '未命名海报'"
|
||||
@ -23,6 +23,7 @@
|
||||
|
||||
<!-- 左栏:配置区 -->
|
||||
<PosterConfigRail
|
||||
ref="configRailRef"
|
||||
v-show="!isMobile || mobileTab === 'config'"
|
||||
:draft="ws.draft.value"
|
||||
@update:draft="onDraftUpdate"
|
||||
@ -41,7 +42,9 @@
|
||||
|
||||
<!-- 右栏:文案与发布(三栏模式直接显示,双栏模式隐藏) -->
|
||||
<PosterInspector
|
||||
v-if="!hideInspector"
|
||||
v-if="isMobile || !hideInspector"
|
||||
ref="inlineInspectorRef"
|
||||
v-show="!isMobile || mobileTab === 'inspector'"
|
||||
:draft="ws.draft.value"
|
||||
@update:draft="onDraftUpdate"
|
||||
/>
|
||||
@ -67,6 +70,7 @@
|
||||
:z-index="100"
|
||||
>
|
||||
<PosterInspector
|
||||
ref="drawerInspectorRef"
|
||||
:draft="ws.draft.value"
|
||||
@update:draft="onDraftUpdate"
|
||||
/>
|
||||
@ -113,26 +117,33 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const ws = usePosterWorkspace()
|
||||
const { isMobile } = useMobile()
|
||||
const workspaceRef = ref<HTMLElement | null>(null)
|
||||
const stageRef = ref<InstanceType<typeof PosterStage> | null>(null)
|
||||
const configRailRef = ref<InstanceType<typeof PosterConfigRail> | null>(null)
|
||||
const inlineInspectorRef = ref<InstanceType<typeof PosterInspector> | null>(null)
|
||||
const drawerInspectorRef = ref<InstanceType<typeof PosterInspector> | null>(null)
|
||||
|
||||
// 独立标题(不污染 productName)
|
||||
const posterTitle = ref('')
|
||||
|
||||
const showTasksDrawer = ref(false)
|
||||
const showInspectorDrawer = ref(false)
|
||||
const mobileTab = ref<'config' | 'stage' | 'inspector'>('stage')
|
||||
const mobileTab = ref<'config' | 'stage' | 'inspector'>('config')
|
||||
|
||||
// ── 布局模式 ─────────────────────────────
|
||||
const workspaceWidth = ref(window.innerWidth - 220)
|
||||
const workspaceWidth = ref(0)
|
||||
let workspaceObserver: ResizeObserver | null = null
|
||||
|
||||
function updateWorkspaceWidth() {
|
||||
workspaceWidth.value = window.innerWidth - 220
|
||||
}
|
||||
onMounted(() => {
|
||||
if (!workspaceRef.value) return
|
||||
workspaceWidth.value = workspaceRef.value.clientWidth
|
||||
workspaceObserver = new ResizeObserver(entries => {
|
||||
workspaceWidth.value = entries[0]?.contentRect.width || workspaceRef.value?.clientWidth || 0
|
||||
})
|
||||
workspaceObserver.observe(workspaceRef.value)
|
||||
})
|
||||
|
||||
onMounted(() => window.addEventListener('resize', updateWorkspaceWidth))
|
||||
onBeforeUnmount(() => window.removeEventListener('resize', updateWorkspaceWidth))
|
||||
|
||||
const hideInspector = computed(() => workspaceWidth.value < 900)
|
||||
const hideInspector = computed(() => workspaceWidth.value > 0 && workspaceWidth.value < 900)
|
||||
|
||||
// ── 草稿更新 ─────────────────────────────
|
||||
function onDraftUpdate(patch: Record<string, any>) {
|
||||
@ -160,53 +171,64 @@ async function onAction() {
|
||||
|
||||
switch (state) {
|
||||
case 'select-product':
|
||||
// 移动端切到配置 tab
|
||||
if (isMobile.value) mobileTab.value = 'config'
|
||||
// 产品面板内部的"选择产品"按钮会打开抽屉
|
||||
ElMessage.info('请点击左栏"产品资料"中的"选择产品"按钮')
|
||||
await nextTick()
|
||||
configRailRef.value?.openProductSelector()
|
||||
break
|
||||
|
||||
case 'upload-data':
|
||||
if (isMobile.value) mobileTab.value = 'config'
|
||||
ElMessage.info('请在左栏"计划书与数据"中上传计划书')
|
||||
await nextTick()
|
||||
configRailRef.value?.openSourceUpload()
|
||||
break
|
||||
|
||||
case 'confirm-data':
|
||||
if (isMobile.value) mobileTab.value = 'config'
|
||||
ElMessage.info('请在左栏"计划书与数据"中确认解析数据')
|
||||
await nextTick()
|
||||
configRailRef.value?.openSourceReview()
|
||||
break
|
||||
|
||||
case 'select-template':
|
||||
if (isMobile.value) mobileTab.value = 'config'
|
||||
ElMessage.info('请在左栏"场景与模板"中选择海报模板')
|
||||
await nextTick()
|
||||
configRailRef.value?.openTemplates()
|
||||
break
|
||||
|
||||
case 'generate-copy':
|
||||
// 打开右侧栏/抽屉并切到文案 tab
|
||||
if (hideInspector.value) {
|
||||
showInspectorDrawer.value = true
|
||||
}
|
||||
if (isMobile.value) mobileTab.value = 'inspector'
|
||||
ElMessage.info('请在右栏"文案"标签中生成文案')
|
||||
await openInspectorTab('copy')
|
||||
break
|
||||
|
||||
case 'confirm-compliance':
|
||||
// 打开右侧栏/抽屉并切到发布 tab
|
||||
if (hideInspector.value) {
|
||||
showInspectorDrawer.value = true
|
||||
}
|
||||
if (isMobile.value) mobileTab.value = 'inspector'
|
||||
// 自动勾选合规确认(用户已走到这一步说明前面都通过了)
|
||||
await nextTick()
|
||||
ElMessage.info('请在"发布"标签中勾选合规确认')
|
||||
await openInspectorTab('delivery')
|
||||
break
|
||||
|
||||
case 'generate-poster':
|
||||
await onGenerate()
|
||||
break
|
||||
|
||||
case 'download-poster':
|
||||
await onDownload()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function openInspectorTab(tab: 'copy' | 'layout' | 'delivery') {
|
||||
if (isMobile.value) {
|
||||
mobileTab.value = 'inspector'
|
||||
await nextTick()
|
||||
inlineInspectorRef.value?.openTab(tab)
|
||||
return
|
||||
}
|
||||
if (hideInspector.value) {
|
||||
showInspectorDrawer.value = true
|
||||
await nextTick()
|
||||
drawerInspectorRef.value?.openTab(tab)
|
||||
return
|
||||
}
|
||||
await nextTick()
|
||||
inlineInspectorRef.value?.openTab(tab)
|
||||
}
|
||||
|
||||
// ── 生成海报 ─────────────────────────────
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let documentSaveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@ -661,6 +683,8 @@ watch(
|
||||
)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
workspaceObserver?.disconnect()
|
||||
workspaceObserver = null
|
||||
stopPolling()
|
||||
if (documentSaveTimer) clearTimeout(documentSaveTimer)
|
||||
if (ws.draft.value.posterUrl) {
|
||||
@ -675,32 +699,42 @@ onBeforeUnmount(() => {
|
||||
<style scoped>
|
||||
/* ── 主题变量 ───────────────────────── */
|
||||
.poster-workspace {
|
||||
--poster-accent: #3b7a57;
|
||||
--poster-accent-light: #f0f7f3;
|
||||
--poster-border: #e5eaf3;
|
||||
--poster-text: #172033;
|
||||
--poster-muted: #667085;
|
||||
--poster-accent: #176b4d;
|
||||
--poster-accent-hover: #10583f;
|
||||
--poster-accent-light: #eaf5ef;
|
||||
--poster-border: #dfe7e2;
|
||||
--poster-text: #17251e;
|
||||
--poster-muted: #66756d;
|
||||
--poster-surface: #fff;
|
||||
--poster-bg: #f3f7f4;
|
||||
--poster-radius: 14px;
|
||||
--poster-shadow: 0 12px 34px rgb(24 61 44 / 8%);
|
||||
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
background: var(--poster-bg);
|
||||
position: relative;
|
||||
color-scheme: light;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
/* ── 工作区主体:三栏 grid ────────────── */
|
||||
.workspace-body {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 280px) 1fr minmax(260px, 320px);
|
||||
grid-template-columns: minmax(260px, 300px) minmax(0, 1fr) minmax(280px, 320px);
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── 隐藏第三栏(inspector)在双栏模式 ── */
|
||||
.workspace-body.hide-inspector {
|
||||
grid-template-columns: minmax(240px, 280px) 1fr;
|
||||
grid-template-columns: minmax(260px, 300px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
/* ── 右侧栏浮动按钮 ────────────────── */
|
||||
@ -711,39 +745,47 @@ onBeforeUnmount(() => {
|
||||
z-index: 90;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
box-shadow: 0 4px 16px rgb(59 122 87 / 30%);
|
||||
box-shadow: 0 10px 28px rgb(23 107 77 / 24%);
|
||||
}
|
||||
|
||||
/* ── 移动端标签页 ────────────────────── */
|
||||
.mobile-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--poster-border);
|
||||
background: #fafbfc;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--poster-border);
|
||||
border-radius: 12px;
|
||||
background: #e9efeb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-tabs button {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
min-height: 38px;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
border-radius: 9px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
color: var(--poster-muted);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.15s;
|
||||
transition: background 0.16s ease, color 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.mobile-tabs button.active {
|
||||
color: var(--poster-accent);
|
||||
border-bottom-color: var(--poster-accent);
|
||||
background: #fff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 2px 8px rgb(24 61 44 / 8%);
|
||||
}
|
||||
|
||||
/* ── 移动端布局 ─────────────────────── */
|
||||
.mobile-mode .workspace-body {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.mobile-mode .workspace-body :deep(.config-rail),
|
||||
@ -752,16 +794,25 @@ onBeforeUnmount(() => {
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
border: none;
|
||||
border-radius: var(--poster-radius);
|
||||
}
|
||||
|
||||
.mobile-mode .workspace-body :deep(.config-rail) {
|
||||
border-bottom: 1px solid var(--poster-border);
|
||||
border: 1px solid var(--poster-border);
|
||||
}
|
||||
|
||||
/* ── 双栏响应式 ──────────────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.workspace-body:not(.mobile-tabs-visible) {
|
||||
grid-template-columns: 1fr;
|
||||
@container (max-width: 899px) {
|
||||
.workspace-body {
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.poster-workspace *,
|
||||
.poster-workspace *::before,
|
||||
.poster-workspace *::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -37,21 +37,48 @@
|
||||
<!-- ===== 工作区主体 ===== -->
|
||||
<template v-else>
|
||||
<div class="ppt-ws__main">
|
||||
<!-- 移动端:步骤导航为横向条(桌面端通过 CSS 隐藏) -->
|
||||
<!-- 移动端:只展示当前步骤,完整步骤放入抽屉,避免小屏横向拥挤 -->
|
||||
<div class="ppt-ws__mobile-nav">
|
||||
<template v-for="(s, idx) in steps" :key="idx">
|
||||
<button class="ppt-ws__mobile-current" type="button" @click="mobileStepsOpen = true">
|
||||
<span class="ppt-ws__mobile-copy">
|
||||
<span class="ppt-ws__mobile-meta">步骤 {{ currentStep + 1 }} / {{ steps.length }}</span>
|
||||
<strong>{{ steps[currentStep].title }}</strong>
|
||||
</span>
|
||||
<span class="ppt-ws__mobile-open">
|
||||
查看流程
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
</button>
|
||||
<div class="ppt-ws__mobile-progress" aria-hidden="true">
|
||||
<span :style="{ width: mobileProgress }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer
|
||||
v-model="mobileStepsOpen"
|
||||
title="PPT 生成流程"
|
||||
direction="btt"
|
||||
size="auto"
|
||||
class="ppt-ws__step-drawer"
|
||||
>
|
||||
<div class="ppt-ws__drawer-steps">
|
||||
<button
|
||||
class="ppt-ws__mobile-step"
|
||||
v-for="(s, idx) in steps"
|
||||
:key="idx"
|
||||
type="button"
|
||||
class="ppt-ws__drawer-step"
|
||||
:class="{ active: idx === currentStep, done: idx < currentStep }"
|
||||
:disabled="idx > maxAccessibleStep"
|
||||
@click="goToStep(idx)"
|
||||
>
|
||||
<span class="ppt-ws__mobile-step-num">{{ idx + 1 }}</span>
|
||||
<span class="ppt-ws__mobile-step-name">{{ s.title }}</span>
|
||||
<span class="ppt-ws__drawer-num">{{ idx < currentStep ? '✓' : idx + 1 }}</span>
|
||||
<span>
|
||||
<strong>{{ s.title }}</strong>
|
||||
<small>{{ s.description }}</small>
|
||||
</span>
|
||||
</button>
|
||||
<span v-if="idx < steps.length - 1" class="ppt-ws__mobile-step-sep" :class="{ done: idx < currentStep }" />
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<!-- 左侧步骤导航(桌面端) -->
|
||||
<PptWorkspaceNav
|
||||
@ -74,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<PptWorkspaceActions :current-step="currentStep" back-label="返回上一步" @back="goToStep(currentStep - 1)">
|
||||
<PptWorkspaceActions v-if="showWorkspaceActions" :current-step="currentStep" back-label="返回上一步" @back="goToStep(currentStep - 1)">
|
||||
<template #right>
|
||||
<span v-if="sessionId" class="ppt-ws__step-hint">
|
||||
第 {{ currentStep + 1 }} / {{ steps.length }} 步 · {{ steps[currentStep].title }}
|
||||
@ -84,61 +111,61 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧上下文面板(仅大屏,可通过关闭按钮隐藏;核对步骤自动隐藏进入专注模式) -->
|
||||
<aside v-if="sessionId && rightPanelOpen && currentStep !== 2" class="ppt-ws__aside">
|
||||
<div class="ppt-ws__aside-header">
|
||||
<span>上下文</span>
|
||||
<el-icon class="ppt-ws__aside-close" @click="rightPanelOpen = false"><Close /></el-icon>
|
||||
</div>
|
||||
<!-- <aside v-if="sessionId && rightPanelOpen && currentStep !== 2" class="ppt-ws__aside">-->
|
||||
<!-- <div class="ppt-ws__aside-header">-->
|
||||
<!-- <span>上下文</span>-->
|
||||
<!-- <el-icon class="ppt-ws__aside-close" @click="rightPanelOpen = false"><Close /></el-icon>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 步骤 0:上传 — 文件信息(暂无 session 时不显示) -->
|
||||
<div v-if="currentStep === 0" class="ppt-ws__aside-section">
|
||||
<p class="ppt-ws__aside-hint">上传文件后,此处显示文件摘要和配置信息。</p>
|
||||
</div>
|
||||
<!-- <!– 步骤 0:上传 — 文件信息(暂无 session 时不显示) –>-->
|
||||
<!-- <div v-if="currentStep === 0" class="ppt-ws__aside-section">-->
|
||||
<!-- <p class="ppt-ws__aside-hint">上传文件后,此处显示文件摘要和配置信息。</p>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 步骤 1:解析 — 解析进度 -->
|
||||
<div v-else-if="currentStep === 1" class="ppt-ws__aside-section">
|
||||
<h4>解析进度</h4>
|
||||
<el-progress :percentage="ws.parseProgress.value" :stroke-width="6" />
|
||||
<p class="ppt-ws__aside-status">{{ ws.serverStatus.value || '等待中' }}</p>
|
||||
</div>
|
||||
<!-- <!– 步骤 1:解析 — 解析进度 –>-->
|
||||
<!-- <div v-else-if="currentStep === 1" class="ppt-ws__aside-section">-->
|
||||
<!-- <h4>解析进度</h4>-->
|
||||
<!-- <el-progress :percentage="ws.parseProgress.value" :stroke-width="6" />-->
|
||||
<!-- <p class="ppt-ws__aside-status">{{ ws.serverStatus.value || '等待中' }}</p>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 步骤 2:数据核对 — 校验状态 -->
|
||||
<div v-else-if="currentStep === 2" class="ppt-ws__aside-section">
|
||||
<h4>校验状态</h4>
|
||||
<div class="ppt-ws__aside-stat">
|
||||
<span class="stat-label">错误</span>
|
||||
<span class="stat-value" :class="{ error: stepErrors[2] > 0 }">{{ stepErrors[2] }}</span>
|
||||
</div>
|
||||
<div class="ppt-ws__aside-stat">
|
||||
<span class="stat-label">警告</span>
|
||||
<span class="stat-value" :class="{ warn: stepWarnings[2] > 0 }">{{ stepWarnings[2] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <!– 步骤 2:数据核对 — 校验状态 –>-->
|
||||
<!-- <div v-else-if="currentStep === 2" class="ppt-ws__aside-section">-->
|
||||
<!-- <h4>校验状态</h4>-->
|
||||
<!-- <div class="ppt-ws__aside-stat">-->
|
||||
<!-- <span class="stat-label">错误</span>-->
|
||||
<!-- <span class="stat-value" :class="{ error: stepErrors[2] > 0 }">{{ stepErrors[2] }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="ppt-ws__aside-stat">-->
|
||||
<!-- <span class="stat-label">警告</span>-->
|
||||
<!-- <span class="stat-value" :class="{ warn: stepWarnings[2] > 0 }">{{ stepWarnings[2] }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 步骤 3:生成 — 配置摘要 -->
|
||||
<div v-else-if="currentStep === 3" class="ppt-ws__aside-section">
|
||||
<h4>生成配置</h4>
|
||||
<div class="ppt-ws__aside-stat">
|
||||
<span class="stat-label">任务 ID</span>
|
||||
<span class="stat-value mono">{{ ws.latestTaskId.value || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <!– 步骤 3:生成 — 配置摘要 –>-->
|
||||
<!-- <div v-else-if="currentStep === 3" class="ppt-ws__aside-section">-->
|
||||
<!-- <h4>生成配置</h4>-->
|
||||
<!-- <div class="ppt-ws__aside-stat">-->
|
||||
<!-- <span class="stat-label">任务 ID</span>-->
|
||||
<!-- <span class="stat-value mono">{{ ws.latestTaskId.value || '—' }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 步骤 4:结果 — 质量状态 -->
|
||||
<div v-else-if="currentStep === 4" class="ppt-ws__aside-section">
|
||||
<h4>质量状态</h4>
|
||||
<p class="ppt-ws__aside-hint">详情请查看右侧质量检查面板。</p>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- <!– 步骤 4:结果 — 质量状态 –>-->
|
||||
<!-- <div v-else-if="currentStep === 4" class="ppt-ws__aside-section">-->
|
||||
<!-- <h4>质量状态</h4>-->
|
||||
<!-- <p class="ppt-ws__aside-hint">详情请查看右侧质量检查面板。</p>-->
|
||||
<!-- </div>-->
|
||||
<!-- </aside>-->
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, provide, onUnmounted } from 'vue'
|
||||
import { computed, onMounted, ref, provide, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Bell, Clock, RefreshLeft, ArrowLeft, Close } from '@element-plus/icons-vue'
|
||||
import { Bell, Clock, RefreshLeft, ArrowLeft, ArrowDown, Close } from '@element-plus/icons-vue'
|
||||
import { usePptWorkspace } from '@/composables/useWorkspace'
|
||||
import { useAutoSave } from '@/composables/useAutoSave'
|
||||
import DraftIndicator from '@/components/DraftIndicator.vue'
|
||||
@ -156,6 +183,7 @@ const ws = usePptWorkspace()
|
||||
const { sessionId, currentStep } = ws
|
||||
const activeSection = ref<'workspace' | 'tasks'>('workspace')
|
||||
const rightPanelOpen = ref(true)
|
||||
const mobileStepsOpen = ref(false)
|
||||
|
||||
// 自动保存
|
||||
const autoSave = useAutoSave({ endpoint: () => `/workspace/ppt/workspaces/${sessionId.value}/draft` })
|
||||
@ -205,6 +233,8 @@ const steps = [
|
||||
const stepErrors = ref<number[]>([0, 0, 0, 0, 0])
|
||||
const stepWarnings = ref<number[]>([0, 0, 0, 0, 0])
|
||||
const maxAccessibleStep = ref(0)
|
||||
const mobileProgress = computed(() => `${((currentStep.value + 1) / steps.length) * 100}%`)
|
||||
const showWorkspaceActions = computed(() => currentStep.value !== 0 && currentStep.value !== 2)
|
||||
|
||||
// 刷新恢复
|
||||
onMounted(async () => {
|
||||
@ -223,6 +253,7 @@ function toggleSection() {
|
||||
function goToStep(idx: number) {
|
||||
if (idx >= 0 && idx <= maxAccessibleStep.value) {
|
||||
ws.goToStep(idx)
|
||||
mobileStepsOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,23 +298,27 @@ function resetAll() {
|
||||
<style scoped>
|
||||
/* ===== CSS 自定义属性 ===== */
|
||||
.ppt-ws {
|
||||
--ws-accent: #3B7A57;
|
||||
--ws-accent-soft: #f0f7f3;
|
||||
--ws-border: #e5e7eb;
|
||||
--ws-text: #1a2e23;
|
||||
--ws-muted: #6b7280;
|
||||
--ws-bg: #f5f5f5;
|
||||
--ws-accent: #176b4d;
|
||||
--ws-accent-hover: #10583f;
|
||||
--ws-accent-soft: #eaf5ef;
|
||||
--ws-border: #dfe7e2;
|
||||
--ws-text: #17251e;
|
||||
--ws-muted: #66756d;
|
||||
--ws-bg: #f3f7f4;
|
||||
--ws-panel: #fff;
|
||||
--ws-radius: 12px;
|
||||
--ws-radius-sm: 8px;
|
||||
--ws-radius: 14px;
|
||||
--ws-radius-sm: 10px;
|
||||
--ws-shadow: 0 12px 34px rgb(24 61 44 / 8%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--ws-bg);
|
||||
font-size: 14px;
|
||||
color: var(--ws-text);
|
||||
container-type: inline-size;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* ===== 顶栏 ===== */
|
||||
@ -292,12 +327,14 @@ function resetAll() {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0 20px;
|
||||
height: 64px;
|
||||
min-height: 64px;
|
||||
background: var(--ws-panel);
|
||||
padding: 0 22px;
|
||||
height: 68px;
|
||||
min-height: 68px;
|
||||
background: rgb(255 255 255 / 96%);
|
||||
border-bottom: 1px solid var(--ws-border);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.ppt-ws__toolbar-left {
|
||||
@ -308,13 +345,17 @@ function resetAll() {
|
||||
}
|
||||
|
||||
.ppt-ws__back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: var(--ws-muted);
|
||||
flex-shrink: 0;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
transition: background 0.16s ease, color 0.16s ease;
|
||||
}
|
||||
|
||||
.ppt-ws__back:hover {
|
||||
@ -323,8 +364,9 @@ function resetAll() {
|
||||
}
|
||||
|
||||
.ppt-ws__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -338,12 +380,26 @@ function resetAll() {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ppt-ws__toolbar-right :deep(.el-button) {
|
||||
min-height: 36px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.ppt-ws__toolbar-right :deep(.el-button--primary) {
|
||||
--el-button-bg-color: var(--ws-accent);
|
||||
--el-button-border-color: var(--ws-accent);
|
||||
--el-button-hover-bg-color: var(--ws-accent-hover);
|
||||
--el-button-hover-border-color: var(--ws-accent-hover);
|
||||
}
|
||||
|
||||
/* ===== 主区域(三栏) ===== */
|
||||
.ppt-ws__main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* ===== 中间区域 ===== */
|
||||
@ -353,8 +409,10 @@ function resetAll() {
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
background: var(--ws-panel);
|
||||
border-left: 1px solid var(--ws-border);
|
||||
border-right: 1px solid var(--ws-border);
|
||||
border: 1px solid var(--ws-border);
|
||||
border-radius: var(--ws-radius);
|
||||
box-shadow: var(--ws-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ppt-ws__content {
|
||||
@ -380,7 +438,9 @@ function resetAll() {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
background: var(--ws-panel);
|
||||
border-left: 1px solid var(--ws-border);
|
||||
border: 1px solid var(--ws-border);
|
||||
border-radius: var(--ws-radius);
|
||||
box-shadow: var(--ws-shadow);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -390,7 +450,7 @@ function resetAll() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px 10px;
|
||||
padding: 15px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--ws-muted);
|
||||
@ -464,6 +524,131 @@ function resetAll() {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-current {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: var(--ws-text);
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-copy strong {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-meta {
|
||||
color: var(--ws-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-open {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--ws-accent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-progress {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin-top: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 3px;
|
||||
background: #e4ece7;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-progress span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--ws-accent);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.ppt-ws__step-drawer) {
|
||||
border-radius: 18px 18px 0 0;
|
||||
}
|
||||
|
||||
:global(.ppt-ws__step-drawer .el-drawer__header) {
|
||||
margin-bottom: 8px;
|
||||
padding: 18px 18px 8px;
|
||||
color: #17251e;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
:global(.ppt-ws__step-drawer .el-drawer__body) {
|
||||
padding: 8px 16px max(18px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-steps {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-step {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
color: var(--ws-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-step:disabled {
|
||||
opacity: 0.42;
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-step.active {
|
||||
border-color: #b8d8c7;
|
||||
background: var(--ws-accent-soft);
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-step > span:last-child {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-step small {
|
||||
margin-top: 2px;
|
||||
color: var(--ws-muted);
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 10px;
|
||||
background: #edf2ef;
|
||||
color: var(--ws-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ppt-ws__drawer-step.active .ppt-ws__drawer-num,
|
||||
.ppt-ws__drawer-step.done .ppt-ws__drawer-num {
|
||||
background: var(--ws-accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ===== 任务中心铺满 ===== */
|
||||
.ppt-ws__tasks {
|
||||
flex: 1;
|
||||
@ -486,15 +671,12 @@ function resetAll() {
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 10px 12px;
|
||||
display: block;
|
||||
padding: 11px 14px 10px;
|
||||
background: var(--ws-panel);
|
||||
border-bottom: 1px solid var(--ws-border);
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--ws-border);
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ppt-ws__center {
|
||||
@ -507,78 +689,36 @@ function resetAll() {
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.ppt-ws__toolbar-right .el-button span {
|
||||
.ppt-ws__toolbar-right :deep(.el-button span) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ppt-ws__toolbar-right {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ppt-ws__toolbar-right :deep(.el-button) {
|
||||
width: 36px;
|
||||
margin-left: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.ppt-ws__title {
|
||||
max-width: min(34vw, 180px);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.ppt-ws__main {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
padding-bottom: max(8px, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.ppt-ws__content {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--ws-muted);
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step.active {
|
||||
background: var(--ws-accent-soft);
|
||||
color: var(--ws-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step.done {
|
||||
color: var(--ws-accent);
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step-num {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #e8efe9;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step.active .ppt-ws__mobile-step-num {
|
||||
background: var(--ws-accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step.done .ppt-ws__mobile-step-num {
|
||||
background: #d1fae5;
|
||||
color: var(--ws-accent);
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step-sep {
|
||||
width: 12px;
|
||||
height: 1px;
|
||||
background: var(--ws-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ppt-ws__mobile-step-sep.done {
|
||||
background: var(--ws-accent);
|
||||
}
|
||||
|
||||
/* ===== 兼容无 container query 的浏览器:fallback 媒体查询 ===== */
|
||||
@supports not (container-type: inline-size) {
|
||||
@media (max-width: 959px) {
|
||||
@ -588,18 +728,26 @@ function resetAll() {
|
||||
.ppt-ws__main { flex-direction: column; }
|
||||
.ppt-ws__main > :first-child:not(.ppt-ws__center) { display: none; }
|
||||
.ppt-ws__mobile-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 10px 12px;
|
||||
display: block;
|
||||
padding: 11px 14px 10px;
|
||||
background: var(--ws-panel);
|
||||
border-bottom: 1px solid var(--ws-border);
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--ws-border);
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ppt-ws__center { border: none; }
|
||||
.ppt-ws__toolbar { padding: 0 12px; height: 56px; min-height: 56px; }
|
||||
.ppt-ws__main { gap: 8px; padding: 8px; }
|
||||
.ppt-ws__content { padding: 12px; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ppt-ws *,
|
||||
.ppt-ws *::before,
|
||||
.ppt-ws *::after {
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,25 +7,7 @@
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 顶部摘要条 -->
|
||||
<div class="review-topbar">
|
||||
<div class="topbar-status" :class="validationStatus">
|
||||
<span class="status-icon">{{ statusMark }}</span>
|
||||
<span class="status-text">{{ statusTitle }}</span>
|
||||
<el-tag :type="validationTagType" size="small" effect="plain">{{ validationLabel }}</el-tag>
|
||||
</div>
|
||||
<div class="topbar-metrics">
|
||||
<span class="topbar-metric">{{ extractions.length }} 产品</span>
|
||||
<span class="topbar-metric" :class="{ error: errorCount > 0 }">{{ errorCount }} 错误</span>
|
||||
<span class="topbar-metric" :class="{ warn: warnCount > 0 }">{{ warnCount }} 警告</span>
|
||||
<span class="topbar-metric">{{ totalRows }} 行数据</span>
|
||||
<el-button size="small" :type="showPdfPreview ? 'primary' : 'default'" plain @click="showPdfPreview = !showPdfPreview">
|
||||
{{ showPdfPreview ? '隐藏 PDF' : '显示 PDF' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 三栏主体 -->
|
||||
<!-- 核对主体:产品切换 + 宽数据编辑区 -->
|
||||
<div class="review-body">
|
||||
<!-- 左栏:产品列表 -->
|
||||
<aside class="review-products">
|
||||
@ -55,18 +37,7 @@
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
<!-- 中栏:PDF 预览(可折叠) -->
|
||||
<div v-if="showPdfPreview && currentExt" class="review-pdf">
|
||||
<PdfPagePreview
|
||||
:session-id="props.sessionId"
|
||||
:extraction-index="selectedIdx"
|
||||
:page-number="previewPage"
|
||||
:pdf-name="currentExt.pdfName"
|
||||
@page-change="previewPage = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 中栏:当前产品编辑区 -->
|
||||
<!-- 当前产品编辑区 -->
|
||||
<div class="review-editor" v-if="currentExt">
|
||||
<div class="editor-header">
|
||||
<el-tag :type="typeTagMap[currentExt.planType] || 'info'" size="small">
|
||||
@ -88,37 +59,67 @@
|
||||
|
||||
<!-- 标签页切换 -->
|
||||
<el-tabs v-model="activeTab" class="editor-tabs">
|
||||
<el-tab-pane label="关键字段" name="fields">
|
||||
<el-tab-pane :label="tabLabel('关键字段', 'fields')" name="fields">
|
||||
<el-form label-position="top" size="default" class="fields-form">
|
||||
<div class="fields-grid">
|
||||
<el-form-item label="产品名称">
|
||||
<el-form-item
|
||||
label="产品名称"
|
||||
:class="fieldIssueClass('product_name', 'product name')"
|
||||
:data-issue="fieldIssueMessage('product_name', 'product name')"
|
||||
>
|
||||
<el-input v-model="currentExt.data.product_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品类型">
|
||||
<el-form-item
|
||||
label="产品类型"
|
||||
:class="fieldIssueClass('product_type', 'product type')"
|
||||
:data-issue="fieldIssueMessage('product_type', 'product type')"
|
||||
>
|
||||
<el-select v-model="currentExt.data.product_type" style="width: 100%">
|
||||
<el-option label="储蓄险" value="savings" />
|
||||
<el-option label="重疾险" value="ci" />
|
||||
<el-option label="IUL" value="iul" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="被保人年龄">
|
||||
<el-form-item
|
||||
label="被保人年龄"
|
||||
:class="fieldIssueClass('insured.age', 'age')"
|
||||
:data-issue="fieldIssueMessage('insured.age', 'age')"
|
||||
>
|
||||
<el-input-number v-model="currentExt.data.insured.age" :min="0" :max="120" />
|
||||
</el-form-item>
|
||||
<el-form-item label="被保人性别">
|
||||
<el-form-item
|
||||
label="被保人性别"
|
||||
:class="fieldIssueClass('insured.gender', 'gender')"
|
||||
:data-issue="fieldIssueMessage('insured.gender', 'gender')"
|
||||
>
|
||||
<el-select v-model="currentExt.data.insured.gender" style="width: 100%">
|
||||
<el-option label="男" value="male" />
|
||||
<el-option label="女" value="female" />
|
||||
<el-option label="未知" value="unknown" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="吸烟状态" :class="{ 'field-missing': currentExt.data.insured.smoker === 'unknown' }">
|
||||
<el-form-item
|
||||
label="吸烟状态"
|
||||
:class="[
|
||||
{ 'field-missing': currentExt.data.insured.smoker === 'unknown' },
|
||||
fieldIssueClass('insured.smoker', 'smoker'),
|
||||
]"
|
||||
:data-issue="fieldIssueMessage('insured.smoker', 'smoker')"
|
||||
>
|
||||
<el-select v-model="currentExt.data.insured.smoker" style="width: 100%">
|
||||
<el-option label="吸烟" value="yes" />
|
||||
<el-option label="不吸烟" value="no" />
|
||||
<el-option label="未知(可继续生成)" value="unknown" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="币种" :class="{ 'field-missing': !currentExt.data.policy.currency }">
|
||||
<el-form-item
|
||||
label="币种"
|
||||
:class="[
|
||||
{ 'field-missing': !currentExt.data.policy.currency },
|
||||
fieldIssueClass('policy.currency', 'currency'),
|
||||
]"
|
||||
:data-issue="fieldIssueMessage('policy.currency', 'currency')"
|
||||
>
|
||||
<el-select v-model="currentExt.data.policy.currency" filterable clearable style="width: 100%">
|
||||
<el-option label="美元(USD)" value="USD" />
|
||||
<el-option label="港币(HKD)" value="HKD" />
|
||||
@ -128,13 +129,28 @@
|
||||
<el-option label="英镑(GBP)" value="GBP" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="年缴保费">
|
||||
<el-form-item
|
||||
label="年缴保费"
|
||||
:class="fieldIssueClass('annual_premium', 'annual premium')"
|
||||
:data-issue="fieldIssueMessage('annual_premium', 'annual premium')"
|
||||
>
|
||||
<el-input-number v-model="currentExt.data.policy.annual_premium" :min="0" :step="1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缴费年期">
|
||||
<el-form-item
|
||||
label="缴费年期"
|
||||
:class="fieldIssueClass('premium_payment_period', 'payment period')"
|
||||
:data-issue="fieldIssueMessage('premium_payment_period', 'payment period')"
|
||||
>
|
||||
<el-input-number v-model="currentExt.data.policy.premium_payment_period" :min="1" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="保额" :class="{ 'field-missing': !currentExt.data.policy.sum_insured }">
|
||||
<el-form-item
|
||||
label="保额"
|
||||
:class="[
|
||||
{ 'field-missing': !currentExt.data.policy.sum_insured },
|
||||
fieldIssueClass('sum_insured', 'sum assured'),
|
||||
]"
|
||||
:data-issue="fieldIssueMessage('sum_insured', 'sum assured')"
|
||||
>
|
||||
<el-input-number v-model="currentExt.data.policy.sum_insured" :min="0" :step="10000" placeholder="请输入保额" />
|
||||
</el-form-item>
|
||||
<el-form-item label="基本计划年保费">
|
||||
@ -150,7 +166,7 @@
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`利益演示(${getBenefitRows(currentExt).length} 行)`" name="benefit">
|
||||
<el-tab-pane :label="tabLabel(`利益演示 · ${getBenefitRows(currentExt).length} 行`, 'benefit')" name="benefit">
|
||||
<div class="table-toolbar">
|
||||
<el-button size="small" type="primary" plain @click="addBenefitRow(currentExt)">新增年度</el-button>
|
||||
<el-button size="small" plain @click="sortRows(getBenefitRows(currentExt))">按年度排序</el-button>
|
||||
@ -251,7 +267,7 @@
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`退保提取(${getWithdrawalRows(currentExt).length} 行)`" name="withdrawal">
|
||||
<el-tab-pane :label="tabLabel(`退保提取 · ${getWithdrawalRows(currentExt).length} 行`, 'withdrawal')" name="withdrawal">
|
||||
<div class="table-toolbar">
|
||||
<el-button size="small" type="primary" plain @click="addWithdrawalRow(currentExt)">新增退保行</el-button>
|
||||
<el-button size="small" plain @click="sortRows(getWithdrawalRows(currentExt))">按年度排序</el-button>
|
||||
@ -261,6 +277,7 @@
|
||||
stripe border size="small"
|
||||
max-height="420"
|
||||
style="width: 100%"
|
||||
:cell-class-name="getCellClass"
|
||||
>
|
||||
<el-table-column prop="policy_year" label="保单年度" width="86" fixed="left">
|
||||
<template #default="{ row }">
|
||||
@ -299,37 +316,6 @@
|
||||
<el-empty v-if="currentExt.status === 'error'" :description="currentExt.error || '解析失败'" />
|
||||
</div>
|
||||
|
||||
<!-- 右栏:校验面板 -->
|
||||
<aside class="review-issues">
|
||||
<h4 class="panel-label">
|
||||
校验状态
|
||||
<el-tag v-if="errorCount > 0" type="danger" size="small" effect="plain">{{ errorCount }}</el-tag>
|
||||
<el-tag v-else-if="warnCount > 0" type="warning" size="small" effect="plain">{{ warnCount }}</el-tag>
|
||||
<el-tag v-else type="success" size="small" effect="plain">通过</el-tag>
|
||||
</h4>
|
||||
|
||||
<div v-if="issues.length" class="issue-list">
|
||||
<button
|
||||
v-for="(issue, idx) in issues"
|
||||
:key="idx"
|
||||
type="button"
|
||||
class="issue-item"
|
||||
:class="issue.severity"
|
||||
@click="jumpToIssue(issue)"
|
||||
>
|
||||
<el-icon v-if="issue.severity === 'error'" color="#dc2626"><CircleCloseFilled /></el-icon>
|
||||
<el-icon v-else color="#b45309"><WarningFilled /></el-icon>
|
||||
<span class="issue-text">
|
||||
<span class="issue-field">{{ issue.field || '待确认' }}</span>
|
||||
<span class="issue-msg">{{ issue.message }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="issues-clean">
|
||||
<el-icon color="#16a34a" :size="20"><CircleCheckFilled /></el-icon>
|
||||
<span>未发现阻断问题</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
@ -352,11 +338,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, inject, watch, nextTick } from 'vue'
|
||||
import { ref, computed, onMounted, inject, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { DataAnalysis, Loading, CircleCloseFilled, WarningFilled, CircleCheckFilled } from '@element-plus/icons-vue'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import { pptApi } from '@/utils/ppt-api'
|
||||
import PdfPagePreview from './PdfPagePreview.vue'
|
||||
|
||||
// 自动保存(由父组件 PptPage 注入)
|
||||
const autoSave = inject<{ scheduleSave: (data: Record<string, any>) => void; registerFlush?: (fn: () => Promise<void>) => void } | null>('pptAutoSave', null)
|
||||
@ -364,7 +349,9 @@ const autoSave = inject<{ scheduleSave: (data: Record<string, any>) => void; reg
|
||||
// 注册 flush 回调,父组件 beforeunload 时可强制保存
|
||||
onMounted(() => {
|
||||
if (autoSave?.registerFlush) {
|
||||
autoSave.registerFlush(() => handleSave(true))
|
||||
autoSave.registerFlush(async () => {
|
||||
await handleSave(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@ -381,8 +368,6 @@ const emit = defineEmits<{
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const tableReadonly = ref(true) // 默认只读模式,减少视觉噪音
|
||||
const showPdfPreview = ref(true) // 默认显示 PDF 预览
|
||||
const previewPage = ref(1) // PDF 预览当前页码
|
||||
const extractions = ref<any[]>([])
|
||||
const issues = ref<Array<{ field: string; severity: string; message: string; extractionIndex?: number; path?: string; section?: string; pdfName?: string }>>([])
|
||||
const originalJson = ref('')
|
||||
@ -411,34 +396,16 @@ const warnCount = computed(() => issues.value.filter(i => i.severity !== 'error'
|
||||
watch([errorCount, warnCount], ([errors, warnings]) => {
|
||||
emit('validation-update', { errors, warnings })
|
||||
}, { immediate: true })
|
||||
const totalRows = computed(() => extractions.value.reduce((sum, ext) => sum + getBenefitRows(ext).length, 0))
|
||||
const validationStatus = computed(() => {
|
||||
if (errorCount.value) return 'error'
|
||||
if (warnCount.value) return 'warn'
|
||||
return 'pass'
|
||||
})
|
||||
const validationTagType = computed(() => {
|
||||
if (validationStatus.value === 'error') return 'danger'
|
||||
if (validationStatus.value === 'warn') return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
const validationLabel = computed(() => {
|
||||
if (validationStatus.value === 'error') return '有错误'
|
||||
if (validationStatus.value === 'warn') return '需确认'
|
||||
return '可生成'
|
||||
})
|
||||
const statusMark = computed(() => {
|
||||
if (validationStatus.value === 'error') return '!'
|
||||
if (validationStatus.value === 'warn') return '?'
|
||||
return '✓'
|
||||
})
|
||||
const statusTitle = computed(() => {
|
||||
if (validationStatus.value === 'error') return '存在阻断问题'
|
||||
if (validationStatus.value === 'warn') return '可以继续,但建议先核对'
|
||||
return '数据通过基础校验'
|
||||
})
|
||||
|
||||
const currentExt = computed(() => extractions.value[selectedIdx.value] || null)
|
||||
const currentIssues = computed(() =>
|
||||
issues.value.filter(issue => issueBelongsToProduct(issue, selectedIdx.value))
|
||||
)
|
||||
const currentMetrics = computed(() => {
|
||||
if (!currentExt.value) return []
|
||||
return getKeyMetrics(currentExt.value)
|
||||
@ -446,75 +413,76 @@ const currentMetrics = computed(() => {
|
||||
const isDirty = computed(() => JSON.stringify(extractions.value) !== originalJson.value)
|
||||
|
||||
function productIssueCount(idx: number, severity: string): number {
|
||||
// 简单匹配:按产品索引过滤 issues(后端 field 格式通常含产品名或索引)
|
||||
const ext = extractions.value[idx]
|
||||
if (!ext) return 0
|
||||
const name = ext.productName || ext.data?.product_name || ''
|
||||
return issues.value.filter(i => {
|
||||
if (i.severity !== severity) return false
|
||||
if (name && i.field?.includes(name)) return true
|
||||
if (i.field?.includes(`[${idx}]`)) return true
|
||||
return false
|
||||
const severityMatched = severity === 'error'
|
||||
? i.severity === 'error'
|
||||
: i.severity !== 'error'
|
||||
return severityMatched && issueBelongsToProduct(i, idx)
|
||||
}).length
|
||||
}
|
||||
|
||||
function issueBelongsToProduct(issue: { field?: string; extractionIndex?: number }, idx: number): boolean {
|
||||
if (issue.extractionIndex !== undefined) return issue.extractionIndex === idx
|
||||
const ext = extractions.value[idx]
|
||||
if (!ext) return false
|
||||
const name = ext.productName || ext.data?.product_name || ''
|
||||
if (name && issue.field?.includes(name)) return true
|
||||
if (issue.field?.includes(`[${idx}]`)) return true
|
||||
return extractions.value.length === 1
|
||||
}
|
||||
|
||||
function findFieldIssue(...tokens: string[]) {
|
||||
const normalizedTokens = tokens.map(token => token.toLowerCase())
|
||||
return currentIssues.value.find(issue => {
|
||||
const haystack = [issue.field, issue.path, issue.section, issue.message]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
return normalizedTokens.some(token => haystack.includes(token))
|
||||
})
|
||||
}
|
||||
|
||||
function fieldIssueClass(...tokens: string[]): Record<string, boolean> {
|
||||
const issue = findFieldIssue(...tokens)
|
||||
return {
|
||||
'field-has-error': issue?.severity === 'error',
|
||||
'field-has-warn': !!issue && issue.severity !== 'error',
|
||||
}
|
||||
}
|
||||
|
||||
function fieldIssueMessage(...tokens: string[]): string {
|
||||
return findFieldIssue(...tokens)?.message || ''
|
||||
}
|
||||
|
||||
function issueSection(issue: { field?: string; path?: string; section?: string; message?: string }): 'fields' | 'benefit' | 'withdrawal' {
|
||||
const haystack = [issue.field, issue.path, issue.section, issue.message]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
if (haystack.includes('withdrawal') || haystack.includes('提取')) return 'withdrawal'
|
||||
if (
|
||||
haystack.includes('benefit')
|
||||
|| haystack.includes('利益')
|
||||
|| haystack.includes('surrender')
|
||||
|| haystack.includes('cash_value')
|
||||
) return 'benefit'
|
||||
return 'fields'
|
||||
}
|
||||
|
||||
function sectionIssueCount(section: 'fields' | 'benefit' | 'withdrawal'): number {
|
||||
return currentIssues.value.filter(issue => issueSection(issue) === section).length
|
||||
}
|
||||
|
||||
function tabLabel(label: string, section: 'fields' | 'benefit' | 'withdrawal'): string {
|
||||
const count = sectionIssueCount(section)
|
||||
return count ? `${label} · ${count} 项待核对` : label
|
||||
}
|
||||
|
||||
function selectProduct(idx: number) {
|
||||
selectedIdx.value = idx
|
||||
activeTab.value = 'fields'
|
||||
}
|
||||
|
||||
function jumpToIssue(issue: { field: string; severity: string; message: string; extractionIndex?: number; section?: string; path?: string }) {
|
||||
// 优先使用后端返回的 extractionIndex 精确定位
|
||||
if (issue.extractionIndex !== undefined && issue.extractionIndex >= 0 && issue.extractionIndex < extractions.value.length) {
|
||||
selectedIdx.value = issue.extractionIndex
|
||||
} else {
|
||||
// 回退:用产品名字符串匹配
|
||||
for (let i = 0; i < extractions.value.length; i++) {
|
||||
const ext = extractions.value[i]
|
||||
const name = ext.productName || ext.data?.product_name || ''
|
||||
if (name && issue.field?.includes(name)) {
|
||||
selectedIdx.value = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 section 字段精确切换 tab
|
||||
const section = issue.section || ''
|
||||
const field = issue.field || ''
|
||||
if (section === 'benefitRows' || field.includes('benefit') || field.includes('退保') || field.includes('surrender') || field.includes('premium')) {
|
||||
activeTab.value = 'benefit'
|
||||
} else if (section === 'withdrawalRows' || field.includes('withdrawal') || field.includes('提取')) {
|
||||
activeTab.value = 'withdrawal'
|
||||
} else {
|
||||
activeTab.value = 'fields'
|
||||
}
|
||||
|
||||
// 滚动到对应字段(如果 path 存在)
|
||||
if (issue.path) {
|
||||
nextTick(() => {
|
||||
const target = document.querySelector(`[data-field="${issue.path}"]`)
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
target.classList.add('field-highlight')
|
||||
setTimeout(() => target.classList.remove('field-highlight'), 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// PDF 预览跳转:尝试从 benefitRows[path] 获取 source_page
|
||||
if (showPdfPreview.value) {
|
||||
const rowMatch = issue.path?.match(/benefitRows\[(\d+)\]/)
|
||||
if (rowMatch) {
|
||||
const rowIdx = parseInt(rowMatch[1])
|
||||
const rows = currentExt.value?.data?.benefit_illustration || []
|
||||
if (rows[rowIdx]?.source_page) {
|
||||
previewPage.value = rows[rowIdx].source_page
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [sessionRes, validateRes]: any[] = await Promise.all([
|
||||
@ -601,11 +569,27 @@ function formatMoney(value: any): string {
|
||||
return num.toLocaleString('zh-CN', { maximumFractionDigits: 0 })
|
||||
}
|
||||
|
||||
function getCellClass({ row, column }: { row: any; column: any }): string {
|
||||
function getCellClass({ row, column, rowIndex }: { row: any; column: any; rowIndex: number }): string {
|
||||
const field = column.property
|
||||
if (!field) return ''
|
||||
const val = row[field]
|
||||
if (val == null || val === '') return 'cell-issue-missing'
|
||||
|
||||
const sectionTokens = activeTab.value === 'withdrawal'
|
||||
? ['withdrawal', '提取']
|
||||
: ['benefit', '利益', 'surrender']
|
||||
const issue = currentIssues.value.find(item => {
|
||||
const haystack = [item.field, item.path, item.section, item.message]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
const sectionMatched = sectionTokens.some(token => haystack.includes(token))
|
||||
const fieldMatched = haystack.includes(String(field).toLowerCase())
|
||||
const rowMatched = !item.path || item.path.includes(`[${rowIndex}]`)
|
||||
return sectionMatched && fieldMatched && rowMatched
|
||||
})
|
||||
if (issue?.severity === 'error') return 'cell-issue-error'
|
||||
if (issue) return 'cell-issue-warn'
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -840,83 +824,30 @@ async function handleConfirm() {
|
||||
<style scoped>
|
||||
.ppt-data-review {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ===== 顶部摘要条 ===== */
|
||||
.review-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e5e7eb;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #3B7A57;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-status.error .status-icon { background: #dc2626; }
|
||||
.topbar-status.warn .status-icon { background: #d97706; }
|
||||
.topbar-status.pass .status-icon { background: #16a34a; }
|
||||
|
||||
.status-text {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a2e23;
|
||||
}
|
||||
|
||||
.topbar-metrics {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.topbar-metric {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.topbar-metric.error { color: #dc2626; font-weight: 600; }
|
||||
.topbar-metric.warn { color: #d97706; font-weight: 600; }
|
||||
|
||||
/* ===== 三栏主体 ===== */
|
||||
/* ===== 核对主体 ===== */
|
||||
.review-body {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===== 左栏:产品列表 ===== */
|
||||
.review-products {
|
||||
width: 200px;
|
||||
width: 190px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
@ -925,11 +856,9 @@ async function handleConfirm() {
|
||||
|
||||
.panel-label {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #47564e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
@ -956,8 +885,8 @@ async function handleConfirm() {
|
||||
}
|
||||
|
||||
.product-item.active {
|
||||
background: #f0f7f3;
|
||||
border-color: #3B7A57;
|
||||
background: #eaf5ef;
|
||||
border-color: #176b4d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@ -1003,21 +932,14 @@ async function handleConfirm() {
|
||||
.product-badge.error { background: #fef2f2; color: #dc2626; }
|
||||
.product-badge.warn { background: #fffbeb; color: #d97706; }
|
||||
|
||||
/* ===== PDF 预览面板 ===== */
|
||||
.review-pdf {
|
||||
width: 360px;
|
||||
min-width: 280px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 中栏:编辑区 ===== */
|
||||
/* ===== 数据编辑区 ===== */
|
||||
.review-editor {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 16px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
@ -1040,7 +962,7 @@ async function handleConfirm() {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.metric-chip {
|
||||
@ -1073,8 +995,8 @@ async function handleConfirm() {
|
||||
|
||||
.fields-form .fields-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: 14px 16px;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
@ -1083,82 +1005,6 @@ async function handleConfirm() {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ===== 右栏:校验面板 ===== */
|
||||
.review-issues {
|
||||
width: 240px;
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.issue-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.issue-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
transition: background 0.12s;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.issue-item:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.issue-item.error { background: #fef2f240; }
|
||||
.issue-item.error:hover { background: #fef2f2; }
|
||||
|
||||
.issue-item.warn { background: #fffbeb40; }
|
||||
.issue-item.warn:hover { background: #fffbeb; }
|
||||
|
||||
.issue-item .el-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.issue-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.issue-field {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.issue-msg {
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.issues-clean {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 8px;
|
||||
color: #16a34a;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 加载态 ===== */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
@ -1190,8 +1036,7 @@ async function handleConfirm() {
|
||||
|
||||
/* ===== 响应式 ===== */
|
||||
@media (max-width: 1100px) {
|
||||
.review-issues { width: 200px; }
|
||||
.review-products { width: 170px; }
|
||||
.review-products { width: 180px; }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@ -1214,48 +1059,53 @@ async function handleConfirm() {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.review-issues {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.review-editor {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 缺失字段高亮 */
|
||||
.field-missing :deep(.el-form-item__label) {
|
||||
color: #f56c6c;
|
||||
/* 字段级校验:问题直接贴在数据上,不再占用独立侧栏 */
|
||||
.field-missing :deep(.el-form-item__label),
|
||||
.field-has-error :deep(.el-form-item__label) {
|
||||
color: #c93636;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-missing :deep(.el-input-number) {
|
||||
border-color: #f56c6c;
|
||||
.field-has-warn :deep(.el-form-item__label) {
|
||||
color: #a65a00;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 问题跳转字段高亮动画 */
|
||||
.field-highlight {
|
||||
animation: highlight-pulse 2s ease-out;
|
||||
.field-missing :deep(.el-input__wrapper),
|
||||
.field-has-error :deep(.el-input__wrapper) {
|
||||
background: #fff8f8;
|
||||
box-shadow: 0 0 0 1px #dc6262 inset !important;
|
||||
}
|
||||
|
||||
@keyframes highlight-pulse {
|
||||
0% { background-color: #fef0f0; box-shadow: 0 0 0 2px #f56c6c; }
|
||||
100% { background-color: transparent; box-shadow: none; }
|
||||
.field-has-warn :deep(.el-input__wrapper) {
|
||||
background: #fffaf1;
|
||||
box-shadow: 0 0 0 1px #d79534 inset !important;
|
||||
}
|
||||
|
||||
.field-has-error::after,
|
||||
.field-has-warn::after {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: -12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
content: attr(data-issue);
|
||||
}
|
||||
|
||||
.field-has-error::after {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
.field-has-warn::after {
|
||||
color: #9a5b0a;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.review-topbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.topbar-metrics {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fields-form .fields-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@ -1287,4 +1137,17 @@ async function handleConfirm() {
|
||||
:deep(.cell-issue-missing) {
|
||||
background-color: #fef0f0 !important;
|
||||
}
|
||||
|
||||
:deep(.cell-issue-error) {
|
||||
color: #b42318;
|
||||
font-weight: 600;
|
||||
background-color: #fff0f0 !important;
|
||||
}
|
||||
|
||||
:deep(.cell-issue-warn) {
|
||||
color: #8a4b00;
|
||||
font-weight: 600;
|
||||
background-color: #fff7e8 !important;
|
||||
}
|
||||
</style>
|
||||
min-height: 28px;
|
||||
|
||||
@ -45,13 +45,17 @@ export interface StepDef {
|
||||
icon: Component
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
withDefaults(defineProps<{
|
||||
currentStep: number
|
||||
maxAccessibleStep: number
|
||||
compact?: boolean
|
||||
stepErrors?: number[]
|
||||
stepWarnings?: number[]
|
||||
}>()
|
||||
}>(), {
|
||||
compact: false,
|
||||
stepErrors: () => [0, 0, 0, 0, 0],
|
||||
stepWarnings: () => [0, 0, 0, 0, 0],
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
'select-step': [step: number]
|
||||
@ -70,14 +74,14 @@ const steps: StepDef[] = [
|
||||
.ws-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 8px;
|
||||
min-width: 180px;
|
||||
gap: 6px;
|
||||
padding: 8px 4px;
|
||||
min-width: 196px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ws-nav.collapsed {
|
||||
min-width: 52px;
|
||||
min-width: 58px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -85,43 +89,47 @@ const steps: StepDef[] = [
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
min-height: 48px;
|
||||
padding: 9px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
color: #5f7a69;
|
||||
transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease;
|
||||
color: #607168;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ws-nav__item:hover:not(:disabled) {
|
||||
background: #f0f7f3;
|
||||
color: #1a2e23;
|
||||
background: #fff;
|
||||
border-color: #dfe7e2;
|
||||
color: #17251e;
|
||||
}
|
||||
|
||||
.ws-nav__item.active {
|
||||
background: #f0f7f3;
|
||||
color: #3B7A57;
|
||||
background: #fff;
|
||||
border-color: #bdd9ca;
|
||||
color: #176b4d;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 8px 24px rgb(24 61 44 / 7%);
|
||||
}
|
||||
|
||||
.ws-nav__item.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
width: 3px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
background: #3B7A57;
|
||||
border-radius: 3px;
|
||||
background: #176b4d;
|
||||
}
|
||||
|
||||
.ws-nav__item.completed {
|
||||
color: #3B7A57;
|
||||
color: #176b4d;
|
||||
}
|
||||
|
||||
.ws-nav__item.locked {
|
||||
@ -141,22 +149,22 @@ const steps: StepDef[] = [
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: #e8efe9;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 9px;
|
||||
background: #e8efeb;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ws-nav__item.active .ws-nav__icon {
|
||||
background: #3B7A57;
|
||||
background: #176b4d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ws-nav__item.completed .ws-nav__icon {
|
||||
background: #d1fae5;
|
||||
color: #3B7A57;
|
||||
background: #dff2e7;
|
||||
color: #176b4d;
|
||||
}
|
||||
|
||||
.ws-nav__item.has-error .ws-nav__icon {
|
||||
@ -183,7 +191,7 @@ const steps: StepDef[] = [
|
||||
|
||||
.ws-nav__desc {
|
||||
font-size: 12px;
|
||||
color: #8b9e93;
|
||||
color: #84938b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@ -212,7 +220,9 @@ const steps: StepDef[] = [
|
||||
}
|
||||
|
||||
.collapsed .ws-nav__item {
|
||||
padding: 10px;
|
||||
width: 48px;
|
||||
min-height: 48px;
|
||||
padding: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user