提交
This commit is contained in:
1 parent
b45ce2c32c
commit
a5210a6a9e
30 files changed
+11989
-5917
No files matched your search
@@ -60,10 +60,12 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const fileList = ref<UploadFile[]>([])
|
||||
/** 本地 URL 列表,避免多文件并行上传完成时基于 stale props 互相覆盖 */
|
||||
const localUrls = ref<string[]>([])
|
||||
|
||||
const isDirectUrl = (value: string) => /^https?:\/\//i.test(value)
|
||||
|
||||
const displayItems = computed(() => parseValue(props.modelValue))
|
||||
const displayItems = computed(() => localUrls.value)
|
||||
|
||||
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','))
|
||||
|
||||
@@ -83,7 +85,7 @@ function listToString(items: string[]) {
|
||||
}
|
||||
|
||||
function syncFileListFromModel() {
|
||||
fileList.value = displayItems.value.map((url, index) => ({
|
||||
fileList.value = localUrls.value.map((url, index) => ({
|
||||
name: url.split('/').pop() || `file-${index + 1}`,
|
||||
url,
|
||||
status: 'success'
|
||||
@@ -92,7 +94,8 @@ function syncFileListFromModel() {
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
(value) => {
|
||||
localUrls.value = parseValue(value)
|
||||
if (props.disabled) return
|
||||
syncFileListFromModel()
|
||||
},
|
||||
@@ -124,7 +127,8 @@ async function handleUploadRequest(options: UploadRequestOptions) {
|
||||
if (!fileUrl) {
|
||||
throw new Error('上传失败')
|
||||
}
|
||||
const next = [...displayItems.value, fileUrl]
|
||||
const next = [...localUrls.value, fileUrl]
|
||||
localUrls.value = next
|
||||
emit('update:modelValue', listToString(next))
|
||||
options.onSuccess?.(res)
|
||||
} catch (error: any) {
|
||||
@@ -136,7 +140,8 @@ async function handleUploadRequest(options: UploadRequestOptions) {
|
||||
function handleRemove(uploadFile: UploadFile) {
|
||||
const removedUrl = uploadFile.url || (uploadFile.response as { fileUrl?: string })?.fileUrl
|
||||
if (!removedUrl) return
|
||||
const next = displayItems.value.filter((url) => url !== removedUrl)
|
||||
const next = localUrls.value.filter((url) => url !== removedUrl)
|
||||
localUrls.value = next
|
||||
emit('update:modelValue', listToString(next))
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import {
|
||||
Plus,
|
||||
Loading,
|
||||
@@ -177,13 +177,23 @@
|
||||
const isPointerInside = ref(false)
|
||||
const isDragOver = ref(false)
|
||||
let dragEnterDepth = 0
|
||||
let clearUploadScheduled = false
|
||||
/**
|
||||
* 本地缓冲:el-upload 多选会同步连续触发 @change;
|
||||
* 若基于尚未回写的 props.modelValue 展开累加,会只留下最后一个文件。
|
||||
*/
|
||||
const files = ref<BxAttachmentItem[]>([])
|
||||
|
||||
const files = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
files.value = Array.isArray(val) ? val.slice() : []
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function syncFiles(next: BxAttachmentItem[]) {
|
||||
files.value = next
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
|
||||
@@ -256,12 +266,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: UploadFile) {
|
||||
const rawFile = uploadFile.raw
|
||||
if (!rawFile) return
|
||||
await uploadRawFile(rawFile)
|
||||
async function scheduleClearUploadFiles() {
|
||||
if (clearUploadScheduled) return
|
||||
clearUploadScheduled = true
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
clearUploadScheduled = false
|
||||
}
|
||||
|
||||
function handleAttachmentChange(uploadFile: UploadFile) {
|
||||
const rawFile = uploadFile.raw
|
||||
if (!rawFile) return
|
||||
void uploadRawFile(rawFile)
|
||||
void scheduleClearUploadFiles()
|
||||
}
|
||||
|
||||
function handlePointerEnter() {
|
||||
|
||||
@@ -95,12 +95,16 @@
|
||||
const isPointerInside = ref(false)
|
||||
const isDragOver = ref(false)
|
||||
let dragEnterDepth = 0
|
||||
let clearUploadScheduled = false
|
||||
const previewUrlMap = ref<Record<string, string>>({})
|
||||
|
||||
const files = computed(() => props.modelValue || [])
|
||||
/**
|
||||
* 本地缓冲:el-upload 多选时会同步连续触发多次 @change,
|
||||
* 若每次都基于尚未回写的 props.modelValue 累加,最终只会留下最后一个文件。
|
||||
*/
|
||||
const localFiles = ref<File[]>([])
|
||||
|
||||
const displayItems = computed<DisplayItem[]>(() =>
|
||||
files.value.map((file, index) => {
|
||||
localFiles.value.map((file, index) => {
|
||||
const key = `${file.name}-${file.size}-${file.lastModified}-${index}`
|
||||
return {
|
||||
key,
|
||||
@@ -119,7 +123,7 @@
|
||||
function rebuildPreviews() {
|
||||
revokeAllPreviews()
|
||||
const next: Record<string, string> = {}
|
||||
files.value.forEach((file, index) => {
|
||||
localFiles.value.forEach((file, index) => {
|
||||
if (!file.type.startsWith('image/')) return
|
||||
const key = `${file.name}-${file.size}-${file.lastModified}-${index}`
|
||||
next[key] = URL.createObjectURL(file)
|
||||
@@ -129,12 +133,17 @@
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => rebuildPreviews(),
|
||||
{ deep: true, immediate: true }
|
||||
(val) => {
|
||||
localFiles.value = Array.isArray(val) ? val.slice() : []
|
||||
rebuildPreviews()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function syncFiles(next: File[]) {
|
||||
localFiles.value = next
|
||||
emit('update:modelValue', next)
|
||||
rebuildPreviews()
|
||||
}
|
||||
|
||||
function isFileAccepted(file: File) {
|
||||
@@ -181,7 +190,7 @@
|
||||
|
||||
function appendFiles(incoming: File[]) {
|
||||
if (!incoming.length || props.disabled) return
|
||||
const next = [...files.value]
|
||||
const next = [...localFiles.value]
|
||||
for (const raw of incoming) {
|
||||
if (next.length >= props.limit) {
|
||||
ElMessage.warning(`最多上传 ${props.limit} 个文件`)
|
||||
@@ -196,16 +205,23 @@
|
||||
syncFiles(next)
|
||||
}
|
||||
|
||||
async function onChange(uploadFile: UploadFile) {
|
||||
async function scheduleClearUploadFiles() {
|
||||
if (clearUploadScheduled) return
|
||||
clearUploadScheduled = true
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
clearUploadScheduled = false
|
||||
}
|
||||
|
||||
function onChange(uploadFile: UploadFile) {
|
||||
const raw = uploadFile.raw
|
||||
if (!raw) return
|
||||
appendFiles([raw])
|
||||
await nextTick()
|
||||
uploadRef.value?.clearFiles()
|
||||
void scheduleClearUploadFiles()
|
||||
}
|
||||
|
||||
function removeAt(index: number) {
|
||||
const next = [...files.value]
|
||||
const next = [...localFiles.value]
|
||||
next.splice(index, 1)
|
||||
syncFiles(next)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
type="button"
|
||||
class="invoice-attach-card"
|
||||
:class="{ 'is-selected': selectedKeys.has(item.key) }"
|
||||
:style="{ '--invoice-group-color': invoiceGroupColor(item.invoiceId) }"
|
||||
@click="toggleOne(item.key)"
|
||||
>
|
||||
<span class="invoice-attach-card__index">{{ item.serial }}</span>
|
||||
@@ -48,7 +49,7 @@
|
||||
</span>
|
||||
<div class="invoice-attach-card__footer">
|
||||
<span class="invoice-attach-card__amount">
|
||||
{{ formatMoneyYuan(item.amount) }} 元
|
||||
整票 {{ formatMoneyYuan(item.amount) }} 元
|
||||
</span>
|
||||
<el-icon
|
||||
class="invoice-attach-card__download"
|
||||
@@ -179,6 +180,25 @@
|
||||
return `${y}年${Number(m)}月`
|
||||
}
|
||||
|
||||
/** 同一发票(同一次上传)共用边框色,不同发票互相区分 */
|
||||
const INVOICE_GROUP_COLORS = [
|
||||
'#0891b2',
|
||||
'#7c3aed',
|
||||
'#d97706',
|
||||
'#db2777',
|
||||
'#2563eb',
|
||||
'#059669',
|
||||
'#ea580c',
|
||||
'#4f46e5'
|
||||
]
|
||||
|
||||
function invoiceGroupColor(invoiceId: string | number) {
|
||||
const s = String(invoiceId || '')
|
||||
let hash = 0
|
||||
for (let i = 0; i < s.length; i++) hash = (hash * 31 + s.charCodeAt(i)) >>> 0
|
||||
return INVOICE_GROUP_COLORS[hash % INVOICE_GROUP_COLORS.length]
|
||||
}
|
||||
|
||||
function invoiceAttachments(row: any): BxAttachmentItem[] {
|
||||
const list = Array.isArray(row?.attachments) ? row.attachments : []
|
||||
if (list.length) {
|
||||
@@ -314,37 +334,34 @@
|
||||
position: relative;
|
||||
width: 140px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border, #d9e8e2);
|
||||
border: 2px solid var(--invoice-group-color, var(--border, #d9e8e2));
|
||||
border-radius: 8px;
|
||||
background: var(--surface, #fff);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
transition: box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.12);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-attach-card__index {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 6px;
|
||||
left: 8px;
|
||||
font-size: 12px;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
border-radius: 8px 0 8px 0;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--primary, #10b981);
|
||||
background: none;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.invoice-attach-card__preview {
|
||||
|
||||
Reference in new issue
Block a user