10.17沟通调整
计划收款信息增加复制操作 上传合同调整为:上传合同、上传PI、上传水单(均为必传,且支持多文件操作与下载)
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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"></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"></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>
|
||||
@@ -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 []
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user