fix(delivery): version status commands and manage three document categories
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import ElementPlus from 'element-plus'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import DeliveryDocumentsDialog from './DeliveryDocumentsDialog.vue'
|
||||
import { commonPhase5Stubs } from '../pages/phase5TestStubs'
|
||||
import { deliveryDocumentCategories, getDeliveryDocuments, saveDeliveryDocuments, uploadDeliveryDocument, type DeliveryDocuments } from '../api/deliveryDocuments'
|
||||
const can = vi.hoisted(() => vi.fn(() => true))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can }) }))
|
||||
vi.mock('../api/deliveryDocuments', async importOriginal => ({ ...await importOriginal<typeof import('../api/deliveryDocuments')>(), getDeliveryDocuments: vi.fn(), saveDeliveryDocuments: vi.fn(), uploadDeliveryDocument: vi.fn(), downloadDeliveryDocument: vi.fn() }))
|
||||
const dialogStub = defineComponent({
|
||||
name: 'AppDialog', props: ['modelValue', 'showConfirm', 'confirmDisabled'], emits: ['confirm', 'update:modelValue'],
|
||||
setup(props, { slots, emit }) { return () => props.modelValue ? h('section', [slots.default?.(), props.showConfirm ? h('button', { class: 'save', disabled: props.confirmDisabled, onClick: () => emit('confirm') }, '保存资料') : null, h('button', { class: 'close', onClick: () => emit('update:modelValue', false) }, '关闭')]) : null },
|
||||
})
|
||||
const uploadStub = defineComponent({
|
||||
name: 'ElUpload', props: ['onChange', 'disabled'],
|
||||
setup(props, { slots }) { return () => h('label', [slots.default?.(), h('input', { type: 'file', disabled: props.disabled, onChange: (event: Event) => props.onChange({ raw: (event.target as HTMLInputElement).files?.[0] }) })]) },
|
||||
})
|
||||
const originalFile = { fileId: '9007199254740995', fileName: 'original.pdf', contentType: 'application/pdf', sizeBytes: 4, uploadedAt: '2026-09-09T09:00:00' }
|
||||
function response(): DeliveryDocuments {
|
||||
return { deliveryNo: 'DL/1', customerId: '100', version: 9, bolNo: 'BOL-1', uploadedCategoryCount: 1, totalCategoryCount: 3, allowedActions: ['MANAGE_DOCUMENTS'], replayed: false, categories: deliveryDocumentCategories.map(item => ({ category: item.category, status: item.category === 'BOL' ? 'UPLOADED' : 'MISSING', fileCount: item.category === 'BOL' ? 1 : 0, files: item.category === 'BOL' ? [{ ...originalFile }] : [] })) }
|
||||
}
|
||||
function mountDialog() {
|
||||
const wrapper = mount(DeliveryDocumentsDialog, { props: { modelValue: true, deliveryNo: 'DL/1', customerId: '100' }, global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, AppDialog: dialogStub, ElUpload: uploadStub } } })
|
||||
return wrapper
|
||||
}
|
||||
async function choose(input: ReturnType<ReturnType<typeof mountDialog>['find']>, file: File) {
|
||||
Object.defineProperty(input.element, 'files', { value: [file], configurable: true })
|
||||
await input.trigger('change'); await flushPromises()
|
||||
}
|
||||
describe('delivery document drafts', () => {
|
||||
beforeEach(() => {
|
||||
can.mockReset().mockReturnValue(true)
|
||||
vi.mocked(getDeliveryDocuments).mockReset().mockResolvedValue(response())
|
||||
vi.mocked(saveDeliveryDocuments).mockReset().mockResolvedValue(response())
|
||||
vi.mocked(uploadDeliveryDocument).mockReset().mockResolvedValue({ ...originalFile, fileId: '9007199254740996', fileName: 'new.pdf' })
|
||||
})
|
||||
it('loads once and stages replacement and deletion until a full versioned save', async () => {
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
expect(getDeliveryDocuments).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('1/3 已上传')
|
||||
await choose(wrapper.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['next'], 'new.pdf'))
|
||||
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).not.toContain('original.pdf')
|
||||
expect(wrapper.text()).toContain('new.pdf')
|
||||
await wrapper.find('[data-category="BOL"] .document-file').findAll('button').find(button => button.text() === '删除')!.trigger('click')
|
||||
expect(wrapper.text()).toContain('0/3 已上传')
|
||||
await wrapper.find('.save').trigger('click'); await flushPromises()
|
||||
expect(saveDeliveryDocuments).toHaveBeenCalledWith('DL/1', '100', { expectedVersion: 9, idempotencyKey: expect.any(String), bolNo: 'BOL-1', documents: deliveryDocumentCategories.map(item => ({ category: item.category, fileIds: [] })) })
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('discards unsaved uploads on cancel and reloads persisted categories on reopening', async () => {
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File(['proof'], 'new.pdf'))
|
||||
expect(wrapper.text()).toContain('2/3 已上传')
|
||||
await wrapper.find('.close').trigger('click')
|
||||
await wrapper.setProps({ modelValue: false }); await wrapper.setProps({ modelValue: true }); await flushPromises()
|
||||
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
expect(wrapper.text()).toContain('1/3 已上传')
|
||||
expect(wrapper.text()).not.toContain('new.pdf')
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('preserves the draft and idempotency key on a failed save, and prevents duplicate submissions', async () => {
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
let reject!: (error: Error) => void
|
||||
vi.mocked(saveDeliveryDocuments).mockReturnValueOnce(new Promise((_resolve, rejectRequest) => { reject = rejectRequest }))
|
||||
await wrapper.find('.save').trigger('click'); await wrapper.find('.save').trigger('click')
|
||||
expect(saveDeliveryDocuments).toHaveBeenCalledTimes(1)
|
||||
reject(new Error('保存失败')); await flushPromises()
|
||||
expect(wrapper.text()).toContain('original.pdf')
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
const key = vi.mocked(saveDeliveryDocuments).mock.calls[0]![2].idempotencyKey
|
||||
await wrapper.find('.save').trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(saveDeliveryDocuments).mock.calls[1]![2].idempotencyKey).toBe(key)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('retains the existing file after replacement upload fails and rejects invalid file sizes', async () => {
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
vi.mocked(uploadDeliveryDocument).mockRejectedValueOnce(new Error('上传失败'))
|
||||
await choose(wrapper.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['data'], 'bad.pdf'))
|
||||
expect(wrapper.text()).toContain('original.pdf')
|
||||
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File([], 'empty.bin'))
|
||||
const oversized = new File(['data'], 'large.bin'); Object.defineProperty(oversized, 'size', { value: 52428801 })
|
||||
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), oversized)
|
||||
expect(uploadDeliveryDocument).toHaveBeenCalledTimes(1)
|
||||
const exact = new File(['data'], 'valid.bin'); Object.defineProperty(exact, 'size', { value: 52428800 })
|
||||
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), exact)
|
||||
expect(uploadDeliveryDocument).toHaveBeenCalledTimes(2)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it.each(['permission', 'action'])('is read-only without the required %s', async kind => {
|
||||
if (kind === 'permission') can.mockReturnValue(false)
|
||||
else vi.mocked(getDeliveryDocuments).mockResolvedValue({ ...response(), allowedActions: [] })
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
expect(wrapper.find('.save').exists()).toBe(false)
|
||||
expect(wrapper.find('input[type="file"]').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('original.pdf')
|
||||
expect(saveDeliveryDocuments).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('blocks retries after a version conflict without losing draft files', async () => {
|
||||
vi.mocked(saveDeliveryDocuments).mockRejectedValue({ isAxiosError: true, response: { status: 409, data: { code: 'A0409', message: '版本已变化', requestId: 'req-1', traceId: 'trace-1' } } })
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
await wrapper.find('.save').trigger('click'); await flushPromises()
|
||||
expect(wrapper.find('.save').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.text()).toContain('original.pdf')
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('limits each category to twenty files while allowing replacement', async () => {
|
||||
const snapshot = response()
|
||||
snapshot.categories.find(item => item.category === 'BOL')!.files = Array.from({ length: 20 }, (_, index) => ({ ...originalFile, fileId: String(index + 1) }))
|
||||
snapshot.categories.find(item => item.category === 'BOL')!.fileCount = 20
|
||||
vi.mocked(getDeliveryDocuments).mockResolvedValue(snapshot)
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
expect(wrapper.find('[data-category="BOL"] header input[type="file"]').attributes('disabled')).toBeDefined()
|
||||
await choose(wrapper.find('[data-category="BOL"] .document-file input[type="file"]'), new File(['data'], 'replace.pdf'))
|
||||
await wrapper.find('.save').trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(saveDeliveryDocuments).mock.calls[0]![2].documents.find(item => item.category === 'BOL')!.fileIds).toHaveLength(20)
|
||||
expect(vi.mocked(saveDeliveryDocuments).mock.calls[0]![2].documents.find(item => item.category === 'BOL')!.fileIds).toContain('9007199254740996')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('ignores a late save response after switching to another customer delivery', async () => {
|
||||
let finish!: (value: DeliveryDocuments) => void
|
||||
vi.mocked(saveDeliveryDocuments).mockReturnValueOnce(new Promise(resolve => { finish = resolve }))
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
await wrapper.find('.save').trigger('click')
|
||||
vi.mocked(getDeliveryDocuments).mockResolvedValue({ ...response(), deliveryNo: 'DL/2', customerId: '200', bolNo: 'BOL-2' })
|
||||
await wrapper.setProps({ deliveryNo: 'DL/2', customerId: '200' }); await flushPromises()
|
||||
finish(response()); await flushPromises()
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect(wrapper.find('.save').exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('ignores a late upload failure after closing and reopening on another delivery', async () => {
|
||||
let fail!: (reason: Error) => void
|
||||
vi.mocked(uploadDeliveryDocument).mockReturnValueOnce(new Promise((_resolve, reject) => { fail = reject }))
|
||||
const wrapper = mountDialog(); await flushPromises()
|
||||
await choose(wrapper.find('[data-category="RECEIPT"] input[type="file"]'), new File(['data'], 'pending.pdf'))
|
||||
await wrapper.setProps({ modelValue: false }); await flushPromises()
|
||||
await wrapper.setProps({ modelValue: true, deliveryNo: 'DL/2', customerId: '200' }); await flushPromises()
|
||||
fail(new Error('old upload failed')); await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('old upload failed')
|
||||
expect(wrapper.text()).toContain('1/3 已上传')
|
||||
expect(wrapper.find('[data-category="RECEIPT"] input[type="file"]').attributes('disabled')).toBeUndefined()
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { deliveryDocumentCategories, getDeliveryDocuments, saveDeliveryDocuments, uploadDeliveryDocument, downloadDeliveryDocument, type DeliveryDocuments, type DeliveryDocumentCategory, type DeliveryDocumentFile } from '../api/deliveryDocuments'
|
||||
|
||||
const props = defineProps<{ deliveryNo: string; customerId: string }>()
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const emit = defineEmits<{ saved: [] }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const snapshot = ref<DeliveryDocuments | null>(null)
|
||||
const draft = ref<Array<{ category: DeliveryDocumentCategory; label: string; files: DeliveryDocumentFile[] }>>([])
|
||||
const bolNo = ref('')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const uploading = ref(false)
|
||||
const downloading = ref(false)
|
||||
const uploadKey = ref(0)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
const conflict = ref(false)
|
||||
const canEdit = computed(() => access.can(permission.deliveryDocuments) && Boolean(snapshot.value?.allowedActions.includes('MANAGE_DOCUMENTS')))
|
||||
const busy = computed(() => saving.value || uploading.value)
|
||||
const uploadedCount = computed(() => draft.value.filter(item => item.files.length > 0).length)
|
||||
let sequence = 0
|
||||
function isCurrent(current: number, deliveryNo: string, customerId: string) { return current === sequence && open.value && props.deliveryNo === deliveryNo && props.customerId === customerId }
|
||||
let attempt: { body: string; key: string } | null = null
|
||||
async function load() {
|
||||
const current = ++sequence
|
||||
loading.value = true; saving.value = false; uploading.value = false; downloading.value = false; error.value = null; conflict.value = false; snapshot.value = null; draft.value = []; attempt = null
|
||||
try {
|
||||
const result = await getDeliveryDocuments(props.deliveryNo, props.customerId)
|
||||
if (current !== sequence || !open.value) return
|
||||
snapshot.value = result; bolNo.value = result.bolNo || ''
|
||||
draft.value = deliveryDocumentCategories.map(item => ({ ...item, files: (result.categories.find(group => group.category === item.category)?.files || []).map(file => ({ ...file })) }))
|
||||
} catch (cause) { if (current === sequence) error.value = getApiErrorDetails(cause, '派送资料加载失败') }
|
||||
finally { if (current === sequence) loading.value = false }
|
||||
}
|
||||
async function reload() {
|
||||
if (busy.value) return
|
||||
if (snapshot.value && canEdit.value) {
|
||||
try { await ElMessageBox.confirm('重新加载将放弃本次未保存的资料修改,是否继续?', '重新加载资料', { type: 'warning' }) } catch { return }
|
||||
}
|
||||
await load()
|
||||
}
|
||||
watch(() => [open.value, props.deliveryNo, props.customerId] as const, ([visible]) => {
|
||||
if (visible && props.deliveryNo && props.customerId) void load()
|
||||
else { sequence++; snapshot.value = null; draft.value = []; error.value = null; bolNo.value = ''; attempt = null }
|
||||
}, { immediate: true })
|
||||
async function upload(category: DeliveryDocumentCategory, file: UploadFile, replaceId?: string) {
|
||||
if (!canEdit.value || busy.value || conflict.value || !file.raw) return
|
||||
const group = draft.value.find(item => item.category === category)
|
||||
if (!group || (!replaceId && group.files.length >= 20)) { ElMessage.warning('每类最多上传20个文件'); return }
|
||||
if (file.raw.size === 0 || file.raw.size > 52428800) { ElMessage.warning('文件不能为空,单个文件不能超过50MiB'); return }
|
||||
uploading.value = true; error.value = null
|
||||
const current = sequence; const { deliveryNo, customerId } = props
|
||||
try {
|
||||
const result = await uploadDeliveryDocument(deliveryNo, customerId, category, file.raw)
|
||||
if (!isCurrent(current, deliveryNo, customerId)) return
|
||||
const index = replaceId ? group.files.findIndex(item => item.fileId === replaceId) : -1
|
||||
if (index >= 0) group.files.splice(index, 1, result)
|
||||
else group.files.push(result)
|
||||
} catch (cause) { if (isCurrent(current, deliveryNo, customerId)) error.value = getApiErrorDetails(cause, '资料上传失败') }
|
||||
finally { if (isCurrent(current, deliveryNo, customerId)) { uploading.value = false; uploadKey.value++ } }
|
||||
}
|
||||
function remove(category: DeliveryDocumentCategory, fileId: string) {
|
||||
if (!canEdit.value || busy.value || conflict.value) return
|
||||
const group = draft.value.find(item => item.category === category)
|
||||
if (group) group.files = group.files.filter(file => file.fileId !== fileId)
|
||||
}
|
||||
function isSaved(category: DeliveryDocumentCategory, fileId: string) { return snapshot.value?.categories.find(item => item.category === category)?.files.some(file => file.fileId === fileId) }
|
||||
async function download(category: DeliveryDocumentCategory, file: DeliveryDocumentFile) {
|
||||
if (downloading.value || !isSaved(category, file.fileId)) return
|
||||
downloading.value = true
|
||||
const current = sequence; const { deliveryNo, customerId } = props
|
||||
try {
|
||||
const blob = await downloadDeliveryDocument(deliveryNo, customerId, category, file.fileId)
|
||||
if (!isCurrent(current, deliveryNo, customerId)) return
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a'); link.href = url; link.download = file.fileName; link.click()
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
} catch (cause) { if (isCurrent(current, deliveryNo, customerId)) error.value = getApiErrorDetails(cause, '文件下载失败') }
|
||||
finally { if (isCurrent(current, deliveryNo, customerId)) downloading.value = false }
|
||||
}
|
||||
async function save() {
|
||||
if (!open.value || !canEdit.value || !snapshot.value || busy.value || conflict.value || loading.value) return
|
||||
const body = { expectedVersion: snapshot.value.version, bolNo: bolNo.value.trim() || null, documents: draft.value.map(item => ({ category: item.category, fileIds: item.files.map(file => file.fileId) })) }
|
||||
const serialized = JSON.stringify(body)
|
||||
if (attempt?.body !== serialized) attempt = { body: serialized, key: crypto.randomUUID() }
|
||||
saving.value = true; error.value = null
|
||||
const current = sequence; const { deliveryNo, customerId } = props
|
||||
try {
|
||||
await saveDeliveryDocuments(deliveryNo, customerId, { ...body, idempotencyKey: attempt.key })
|
||||
if (!isCurrent(current, deliveryNo, customerId)) return
|
||||
open.value = false; emit('saved'); ElMessage.success('派送资料已保存')
|
||||
} catch (cause) { if (isCurrent(current, deliveryNo, customerId)) { error.value = getApiErrorDetails(cause, '派送资料保存失败'); conflict.value = error.value.code === 'A0409' } }
|
||||
finally { if (current === sequence) saving.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog v-model="open" title="派送单所需资料" width="min(1040px, calc(100vw - 40px))" confirm-text="保存资料" cancel-text="关闭" :loading="loading" :show-confirm="canEdit" :confirm-loading="busy" :confirm-disabled="conflict || loading" @confirm="save">
|
||||
<div class="delivery-documents">
|
||||
<div class="document-identifiers"><label>派送单号<el-input :model-value="deliveryNo" readonly /></label><label>BOL号<el-input v-model="bolNo" :readonly="!canEdit" :disabled="busy || conflict" placeholder="未填写" /></label></div>
|
||||
<LoadErrorState v-if="error" compact title="派送资料操作失败" :message="error.message" :show-retry="!snapshot || conflict" @retry="reload"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<template v-if="snapshot">
|
||||
<div class="document-summary"><span>{{ canEdit ? '上传、替换和删除将在保存资料后生效;关闭将放弃未保存修改。' : '当前账号仅可查看已保存资料。' }}</span><strong>{{ uploadedCount }}/3 已上传</strong></div>
|
||||
<div class="document-groups">
|
||||
<section v-for="group in draft" :key="group.category" class="document-group" :data-category="group.category">
|
||||
<header><strong>{{ group.label }}</strong><el-tag :type="group.files.length ? 'success' : 'info'">{{ group.files.length ? '已上传' : '待上传' }}</el-tag><span>{{ group.files.length }}/20</span><el-upload v-if="canEdit" :key="uploadKey" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict || group.files.length >= 20" :on-change="(file: UploadFile) => upload(group.category, file)"><el-button :disabled="busy || conflict || group.files.length >= 20">上传</el-button></el-upload></header>
|
||||
<p v-if="!group.files.length" class="document-empty">尚未上传文件</p>
|
||||
<div v-for="file in group.files" :key="file.fileId" class="document-file">
|
||||
<div class="document-file-info"><button type="button" class="document-file-name" :title="file.fileName" :disabled="downloading || !isSaved(group.category, file.fileId)" @click="download(group.category, file)">{{ file.fileName }}</button><small>{{ file.uploadedAt?.replace('T', ' ') || '—' }}{{ isSaved(group.category, file.fileId) ? '' : ' · 待保存' }}</small></div>
|
||||
<template v-if="canEdit"><el-upload :key="uploadKey" :auto-upload="false" :show-file-list="false" :disabled="busy || conflict" :on-change="(uploadFile: UploadFile) => upload(group.category, uploadFile, file.fileId)"><el-button :disabled="busy || conflict">替换</el-button></el-upload><el-button type="danger" link :disabled="busy || conflict" @click="remove(group.category, file.fileId)">删除</el-button></template>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.delivery-documents { display:grid; gap:16px; max-height:66vh; overflow-y:auto; padding:2px 6px 4px 0; }
|
||||
.document-identifiers { display:grid; grid-template-columns:1fr 1fr; gap:18px; }.document-identifiers label { display:grid; gap:8px; font-weight:600; }
|
||||
.document-summary { display:flex; justify-content:space-between; align-items:center; gap:20px; padding:12px 16px; background:var(--el-color-primary-light-9); color:var(--el-color-primary); border-radius:8px; }.document-summary strong { white-space:nowrap; }
|
||||
.document-groups { border:1px solid var(--cc-border); border-radius:8px; overflow:hidden; }.document-group { padding:14px 16px; }.document-group + .document-group { border-top:1px solid var(--cc-border); }
|
||||
.document-group header { display:flex; align-items:center; gap:10px; }.document-group header > span { color:var(--cc-muted); font-size:12px; }.document-group header :deep(.el-upload) { margin-left:auto; }.document-group header > :last-child { margin-left:auto; }
|
||||
.document-file { display:flex; align-items:center; gap:14px; padding-top:10px; }.document-file-info { display:grid; min-width:0; flex:1; gap:4px; }.document-file-name { border:0; background:none; padding:0; text-align:left; color:var(--cc-primary); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; cursor:pointer; font:inherit; }.document-file-name:disabled { color:var(--cc-muted); cursor:default; }.document-file-info small,.document-empty { color:var(--cc-muted); }.document-empty { margin:10px 0 0; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user