10.17沟通调整

计划收款信息增加复制操作
上传合同调整为:上传合同、上传PI、上传水单(均为必传,且支持多文件操作与下载)
This commit is contained in:
浅念
2025-10-22 11:52:20 +08:00
parent 222d07d483
commit ff0fb83bd0
20 changed files with 2139 additions and 856 deletions
+12 -2
View File
@@ -24,7 +24,9 @@
</div>
</template>
<script setup lang="ts">
<script lang="ts">
import { defineComponent } from 'vue'
interface ApprovalRecordItem {
name?: string
role?: string
@@ -36,7 +38,15 @@
comment?: string
}
defineProps<{ records: ApprovalRecordItem[] }>()
export default defineComponent({
name: 'ApprovalTimeline',
props: {
records: {
type: Array as () => ApprovalRecordItem[],
default: () => []
}
}
})
</script>
<style scoped lang="scss">
+311
View File
@@ -0,0 +1,311 @@
<template>
<div class="file-preview-container">
<div class="form-title">
<span class="title">{{ title }}</span>
</div>
<div class="file-preview">
<div v-if="files.length > 0" class="files-list">
<div v-for="(file, index) in files" :key="index" class="file-item">
<div v-if="isImageFile(file)" class="image-preview">
<img
:src="file.url"
:alt="file.name"
class="preview-image"
@click="previewImage(file)"
/>
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<div class="file-actions">
<!-- <el-button type="text" size="small" @click="previewImage(file)">预览</el-button> -->
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
</div>
</div>
</div>
<div v-else class="document-preview">
<div class="file-icon">
<i class="iconfont-sys">&#xe814;</i>
</div>
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(file)">下载</el-button>
</div>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>{{ emptyText }}</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ElMessage } from 'element-plus'
interface FileItem {
name: string
url: string
file_name: string
}
export default defineComponent({
name: 'FilePreview',
props: {
title: {
type: String,
required: true
},
files: {
type: Array as () => FileItem[],
default: () => []
},
emptyText: {
type: String,
default: '暂无文件'
}
},
setup() {
// 判断是否为图片文件
const isImageFile = (file: FileItem) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(file.name)) {
return true
}
// 检查file_name字段
if (file.file_name && file.file_name.startsWith('image/')) {
return true
}
// 检查URL中的文件扩展名(去除查询参数)
if (file.url) {
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
if (/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: FileItem) => {
// 创建图片预览弹窗
const imagePreview = document.createElement('div')
imagePreview.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
cursor: pointer;
`
const image = document.createElement('img')
image.src = file.url
image.alt = file.name
image.style.cssText = `
max-width: 90%;
max-height: 90%;
object-fit: contain;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
`
const closeBtn = document.createElement('div')
closeBtn.innerHTML = '×'
closeBtn.style.cssText = `
position: absolute;
top: 20px;
right: 30px;
color: white;
font-size: 30px;
cursor: pointer;
z-index: 10000;
`
imagePreview.appendChild(image)
imagePreview.appendChild(closeBtn)
document.body.appendChild(imagePreview)
// 点击关闭
const closePreview = () => {
document.body.removeChild(imagePreview)
}
imagePreview.addEventListener('click', closePreview)
closeBtn.addEventListener('click', (e) => {
e.stopPropagation()
closePreview()
})
// ESC键关闭
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closePreview()
document.removeEventListener('keydown', handleKeydown)
}
}
document.addEventListener('keydown', handleKeydown)
}
// 下载文件
const downloadFile = async (file: FileItem) => {
try {
// 对所有文件类型都使用 fetch 下载,确保真正的下载
const response = await fetch(file.url, {
method: 'GET',
headers: {
Accept: '*/*'
}
})
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
}
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = file.name || file.file_name || 'download'
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('下载成功')
} catch (error) {
console.error('下载失败:', error)
const errorMessage = error instanceof Error ? error.message : '未知错误'
ElMessage.error(`下载失败: ${errorMessage}`)
}
}
return {
isImageFile,
previewImage,
downloadFile
}
}
})
</script>
<style scoped lang="scss">
.file-preview-container {
.form-title {
font-weight: bold;
color: #222;
margin-bottom: 15px;
.title {
margin-left: 6px;
}
}
.no-file {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 14px;
}
.no-file i {
font-size: 48px;
color: #ddd;
}
.files-list {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.file-item {
flex: 0 0 auto;
min-width: 200px;
max-width: 300px;
}
.file-preview {
position: relative;
width: 100%;
max-height: 120px;
padding: 10px;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
background: #fafafa;
}
.image-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.preview-image {
flex: 1;
max-height: 70px;
object-fit: cover;
cursor: pointer;
transition: transform 0.2s;
}
.preview-image:hover {
transform: scale(1.05);
}
.document-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.file-icon {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.file-icon i {
font-size: 48px;
color: #409eff;
}
.file-info {
padding: 8px;
background: rgba(255, 255, 255, 0.9);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.file-actions {
display: flex;
gap: 8px;
align-items: center;
}
.file-name {
font-size: 12px;
color: #606266;
text-align: center;
word-break: break-all;
max-width: 100%;
}
}
</style>
+40 -19
View File
@@ -60,9 +60,9 @@
<template #default>
<ElTableColumn type="expand" width="30">
<template #default="scope">
<div style="padding: 6px">
<strong>备注:</strong>
<span>{{ scope.row.remark || '暂无备注' }}</span>
<div style="padding: 6px;display: flex">
<div><strong>备注:</strong></div>
<div><span v-html="scope.row.remark || '暂无备注'"></span></div>
</div>
</template>
</ElTableColumn>
@@ -78,14 +78,17 @@
<ElTableColumn label="数量/台" width="90" align="center">
<template #default="scope">{{ scope.row.num ?? scope.row.quantity ?? 1 }}</template>
</ElTableColumn>
<ElTableColumn label="颜色/外" align="center" prop="color_exterior_snapshot" />
<ElTableColumn
label="内饰"
min-width="120"
align="center"
prop="color_interior_snapshot"
/>
<ElTableColumn label="改装" min-width="120" align="center" prop="modify" />
<ElTableColumn label="颜色" min-width="120"align="center">
<template #default="scope">
<div>
外饰:{{ scope.row.color_exterior_snapshot || '—' }}
</div>
<div>
内饰:{{ scope.row.color_interior_snapshot || '—' }}
</div>
</template>
</ElTableColumn>
<!-- <ElTableColumn label="改装" min-width="120" align="center" prop="modify" /> -->
<ElTableColumn label="官方指导价/二手挂牌价" min-width="175" align="center">
<template #default="scope">
<span v-if="scope.row.mileage > 0">
@@ -142,6 +145,14 @@
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="配件费" min-width="120" align="center">
<template #default="scope">{{
scope.row.accessory_price?.currency_info ||
scope.row.accessory_price?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="FOB运费" min-width="120" align="center">
<template #default="scope">{{
scope.row.fob_transport_fee?.currency_info ||
@@ -174,6 +185,14 @@
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="挂账" min-width="120" align="center">
<template #default="scope">{{
scope.row.retention_price?.currency_info ||
scope.row.retention_price?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="购置税" min-width="120" align="center">
<template #default="scope">{{
scope.row.purchase_tax?.currency_info ||
@@ -186,11 +205,17 @@
<template #default="scope">{{
scope.row.tax_refund?.currency_info ||
scope.row.tax_refund?.amount ||
scope.row.discount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="单价" min-width="120" align="center">
<ElTableColumn label="车价" min-width="120" align="center">
<template #default="scope">{{
scope.row.car_price?.currency_info ||
scope.row.car_price?.amount ||
'—'
}}</template>
</ElTableColumn>
<ElTableColumn label="收款价" min-width="120" align="center">
<template #default="scope">{{
scope.row.unit_price?.currency_info ||
scope.row.unit_price?.amount ||
@@ -233,11 +258,7 @@
</div>
<div class="summary-item">
<span class="label">合计到岸价:</span>
<span class="value"
>${{
formatPrice(Number(orderData.cif_total_amount?.amount ?? orderData.totalCif ?? 0))
}}</span
>
<span class="value"> {{ orderData.cif_total?.currency_info || '—' }}</span>
</div>
<div class="summary-item">
<span class="label">利润:</span>
@@ -355,7 +376,7 @@
currency: p.currency_name_snapshot || '—',
account: p.account_name_snapshot || '—',
remitter: p.remitter_name || '—',
amount: `$${formatPrice(Number(p.amount || 0))}`
amount: p.amount || '—'
}))
: (props.orderData as any).payments || []
if (!list.length) return []
+219
View File
@@ -0,0 +1,219 @@
<template>
<div class="print-preview-container">
<div class="preview-header">
<div class="header-actions">
<el-button @click="download" v-if="showDownload">下载</el-button>
<el-button @click="print" type="primary">打印</el-button>
<el-button @click="close">关闭</el-button>
</div>
</div>
<div class="preview-content">
<div
v-if="pdf.images.length"
class="images-container"
>
<img
v-for="(img, idx) in pdf.images"
:key="idx"
:src="img"
class="preview-image"
/>
</div>
<iframe
v-else-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
class="preview-iframe"
@load="onIframeLoad"
@error="onIframeError"
/>
<div v-else class="no-content">
<p>没有可预览的内容</p>
<p>传入的 PDF URL: {{ pdfUrl }}</p>
<p>PDF Object URL: {{ pdf.url }}</p>
<p>PDF Blob URL: {{ pdf.blobUrl }}</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue'
import { usePdfPreview } from '@/composables/usePdfPreview'
export default defineComponent({
name: 'PrintPreview',
props: {
visible: {
type: Boolean,
default: false
},
showDownload: {
type: Boolean,
default: true
},
pdfUrl: {
type: String,
default: ''
}
},
emits: ['close'],
setup(props, { emit }) {
// 使用公共的 usePdfPreview
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
// 监听 visible 变化,当显示时加载 PDF
watch(() => props.visible, (newVisible) => {
console.log('PrintPreview visible 变化:', newVisible)
if (newVisible && props.pdfUrl) {
// 构造响应对象,模拟 API 返回格式
const response = {
url: props.pdfUrl,
file_url: props.pdfUrl,
file_url_full: props.pdfUrl
}
openWithResponse(response)
console.log('PrintPreview 设置 PDF URL:', props.pdfUrl)
}
})
// 监听 pdfUrl 变化
watch(() => props.pdfUrl, (newUrl) => {
console.log('PrintPreview pdfUrl 变化:', newUrl)
if (newUrl) {
// 构造响应对象,模拟 API 返回格式
const response = {
url: newUrl,
file_url: newUrl,
file_url_full: newUrl
}
openWithResponse(response)
console.log('PrintPreview 更新 PDF URL:', newUrl)
}
})
const close = () => {
emit('close')
}
// iframe 加载事件
const onIframeLoad = () => {
console.log('iframe 加载完成')
}
// iframe 错误事件
const onIframeError = (error: any) => {
console.error('iframe 加载错误:', error)
}
return {
frameRef,
pdf,
download,
print,
close,
onIframeLoad,
onIframeError
}
}
})
</script>
<style scoped lang="scss">
.print-preview-container {
display: flex;
flex-direction: column;
height: 100%;
max-height: 80vh;
}
.preview-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
background: #fafafa;
border-radius: 6px 6px 0 0;
}
.header-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.preview-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.images-container {
overflow: auto;
border: 1px solid #eee;
border-radius: 6px;
padding: 8px;
background: #fff;
}
.preview-image {
display: block;
width: 100%;
margin-bottom: 8px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.preview-iframe {
flex: 1;
width: 100%;
border: 1px solid #eee;
border-radius: 6px;
background: #fff;
}
.no-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #666;
font-size: 14px;
p {
margin: 5px 0;
}
}
// 打印样式
@media print {
.print-preview-container {
max-height: none;
height: auto;
}
.preview-header {
display: none;
}
.preview-content {
height: auto;
}
.images-container {
border: none;
padding: 0;
}
.preview-image {
margin-bottom: 0;
box-shadow: none;
}
.preview-iframe {
border: none;
}
}
</style>