备货单 加 审批流程

This commit is contained in:
浅念
2025-09-08 19:09:55 +08:00
parent 9bd7c89cb4
commit 64e53b6ee9
36 changed files with 11717 additions and 2552 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+283
View File
@@ -0,0 +1,283 @@
<template>
<div class="color-page">
<div class="color-toolbar boxshadow">
<div class="color-search-group">
<el-form-item label="关键字:" label-width="70px">
<el-input
style="min-width: 120px"
v-model="keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getColorList"
/>
</el-form-item>
</div>
</div>
<div class="boxshadow">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="addfn()" v-ripple v-if="hasAuth('approval_list@add')" type="primary">
新增审核流程
</ElButton>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="名称" prop="name" min-width="120" />
<ElTableColumn label="标识" prop="type" width="120" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.is_active ? 'success' : 'info'">
{{ scope.row.is_active ? '启用' : '停用' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
<el-button
type="danger"
link
@click="deleteColor(scope.row)"
v-if="hasAuth('approval_list@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- 新增/编辑对话框 -->
<ElDialog v-model="dialogVisible" title="新增审批流程" align-center width="600px">
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
<ElFormItem label="名称" prop="name">
<ElInput v-model="form.name" placeholder="请输入名称" />
</ElFormItem>
<ElFormItem label="标识" prop="type">
<ElInput v-model="form.type" placeholder="请输入标识" />
</ElFormItem>
<ElFormItem label="超时时间" prop="timeout_hours">
<ElInput v-model="form.timeout_hours" placeholder="请输入超时时间" >
<template #suffix>
<span>h</span>
</template>
</ElInput>
</ElFormItem>
<ElFormItem label="超时动作" prop="timeout_action">
<el-select v-model="form.timeout_action" placeholder="请选择颜色类型" style="width: 100%">
<el-option label="外观颜色" :value="1" />
<el-option label="内饰颜色" :value="2" />
</el-select>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.is_active" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { router } from '@/router'
const { hasAuth } = useAuth()
defineOptions({ name: 'VehicleColor' })
// 响应式数据
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const typeFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
// 表单数据
const form = reactive<any>({
name: '',
type: '',
timeout_hours: '',
timeout_action: '',
is_active: 1
})
// 表单验证规则
const rules = reactive<FormRules>({
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
})
// 分页数据
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
const tableRef = ref()
// 生命周期
onMounted(() => {
getColorList()
})
// 监听搜索条件变化
watch([keyword, typeFilter, statusFilter], () => {
pagination.currentPage = 1
getColorList()
})
// 新增按钮点击
const addfn = () => {
router.push('/admin/approval/approval_add')
}
// 获取颜色列表
const getColorList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await manageService.getstocklist(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取颜色列表失败:', error)
ElMessage.error('获取颜色列表失败')
} finally {
loading.value = false
}
}
// 重置搜索条件
const resetSearch = () => {
keyword.value = ''
typeFilter.value = undefined
statusFilter.value = undefined
pagination.currentPage = 1
getColorList()
}
// 处理分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getColorList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getColorList()
}
// 显示对话框
const showDialog = (type: string, row?: any) => {
router.push(`/admin/approval/approval_edit?id=${row.id}`)
}
// 删除
const deleteColor = (row: any) => {
ElMessageBox.confirm('确定删除该审核流程吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await manageService.stockapprovaldelete({ id: row.id })
ElMessage.success(message)
getColorList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
// 提交表单
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
// 新增
const { message } = await manageService.stockapproval(form)
ElMessage.success(message)
dialogVisible.value = false
getColorList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.color-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.color-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.color-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+3 -1
View File
@@ -349,7 +349,8 @@
dict_code: typeForm.dict_code,
dict_name: typeForm.dict_name,
description: typeForm.description,
status: typeForm.status
status: typeForm.status,
category_id: typeForm.category_id
})
ElMessage.success(message)
} else {
@@ -358,6 +359,7 @@
dict_code: typeForm.dict_code,
dict_name: typeForm.dict_name,
description: typeForm.description,
category_id: typeForm.category_id,
status: typeForm.status
})
ElMessage.success(message)
+34 -3
View File
@@ -116,6 +116,19 @@
<ElFormItem label="邮箱" prop="email">
<ElInput v-model="form.email" :disabled="!isEdit" />
</ElFormItem>
<ElFormItem label="签名" prop="signature" class="right-input">
<template v-if="form.signature_pic_full">
<img :src="form.signature_pic_full" class="signature-img" />
</template>
<template v-else>
<div class="signature-qrcode">
<qrcode-vue :value="qrcodeUrl" :size="120" level="M" />
<div class="qrcode-tip">手机扫码进行签名</div>
</div>
</template>
<el-button @click="handleSignature" type="primary">签名</el-button>
<el-input type="hidden" v-model="form.signature" />
</ElFormItem>
</ElRow>
<!-- <ElRow>
<ElFormItem
@@ -218,6 +231,8 @@
import { UserService } from '@/api/usersApi'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { useUserStore } from '@/store/modules/user'
import { computed } from 'vue'
import QrcodeVue from 'qrcode.vue'
const { uploadHeaders } = useUploadHeaders()
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
defineOptions({ name: 'UserCenter' })
@@ -277,6 +292,22 @@
// await getarea()
await getuserInfo()
})
const qrcodeUrl = computed(() => {
const userId = form.value?.id || userInfo.value?.id || ''
const base = location.origin
const token = (useUserStore() as any).accessToken || ''
const params = new URLSearchParams({ userId: String(userId) })
if (token) params.append('token', token)
return `${base}/auth/signature/mobile?${params.toString()}`
})
const handleSignature = async () => {
const userId = form.value?.id || userInfo.value?.id || ''
const base = location.origin
const token = (useUserStore() as any).accessToken || ''
const params = new URLSearchParams({ userId: String(userId) })
if (token) params.append('token', token)
window.location.href = `${base}/auth/signature/mobile?${params.toString()}`
}
const edit = async () => {
if (isEdit.value) {
const res = await UserService.editUserInfo(form.value)
@@ -328,7 +359,7 @@
</style>
<style lang="scss" scoped>
:deep(.qrcode-radio-group .el-form-item__content){
:deep(.qrcode-radio-group .el-form-item__content) {
height: 40px;
}
.avatar {
@@ -337,7 +368,7 @@
border-radius: 50%;
object-fit: cover;
}
.qrcode-img{
.qrcode-img {
width: 80px;
height: 80px;
}
@@ -372,7 +403,7 @@
overflow: hidden;
text-align: center;
// background: var(--art-main-bg-color);
background: linear-gradient(180deg, #E9F0FF 0%, #FFFFFF 67.9%);
background: linear-gradient(180deg, #e9f0ff 0%, #ffffff 67.9%);
border-radius: $box-radius;
.avatar {
+2 -2
View File
@@ -36,11 +36,11 @@
<div class="form">
<!-- 登录方式切换 tab -->
<div class="login-type-tabs">
<!-- <span
<span
:class="{ active: activeLoginType === 'password' }"
@click="activeLoginType = 'password'"
>账号密码登录</span
> -->
>
<span
:class="{ active: activeLoginType === 'qrcode' }"
@click="activeLoginType = 'qrcode'"
+237
View File
@@ -0,0 +1,237 @@
<template>
<div class="signature-page" :class="{ landscape }">
<div class="header">电子签名</div>
<div class="canvas-wrap">
<canvas
ref="canvasRef"
class="canvas"
@touchstart.prevent="onStart"
@touchmove.prevent="onMove"
@touchend.prevent="onEnd"
@mousedown.prevent="onMouseDown"
@mousemove.prevent="onMouseMove"
@mouseup.prevent="onMouseUp"
></canvas>
</div>
<div class="toolbar">
<el-button @click="clearCanvas">清空</el-button>
<el-button @click="toggleLandscape">横屏</el-button>
<el-button type="primary" :loading="submitting" @click="saveSignature">保存</el-button>
</div>
<div class="tip">请在画布上手写签名,然后点击保存</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useUploadHeaders } from '@/composables/useUploadHeaders'
import { CrmService } from '@/api/crmApi'
import { useUserStore } from '@/store/modules/user'
const route = useRoute()
const userId = route.query.userId as string | undefined
const userStore = useUserStore()
// 优先使用URL传入的token,否则取当前应用内的token
const urlToken = route.query.token as string | undefined
const effectiveToken = urlToken || userStore.accessToken
const { uploadHeaders } = useUploadHeaders()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasRenderingContext2D | null>(null)
const drawing = ref(false)
const submitting = ref(false)
const landscape = ref(false)
const resizeCanvas = () => {
const canvas = canvasRef.value
if (!canvas) return
const dpr = window.devicePixelRatio || 1
let width = Math.min(window.innerWidth - 32, 800)
let height = Math.round(width * 0.5)
if (landscape.value && window.innerHeight > 0) {
// 横屏时尽量占满高度
const h = Math.min(window.innerHeight - 120, 500)
height = h
width = Math.round(h * 2)
}
canvas.style.width = width + 'px'
canvas.style.height = height + 'px'
canvas.width = Math.floor(width * dpr)
canvas.height = Math.floor(height * dpr)
const c = canvas.getContext('2d')!
c.scale(dpr, dpr)
c.lineWidth = 3
c.lineCap = 'round'
c.lineJoin = 'round'
c.strokeStyle = '#111'
ctx.value = c
// 背景白色
c.fillStyle = '#fff'
c.fillRect(0, 0, width, height)
}
onMounted(() => {
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
})
const getPoint = (e: TouchEvent | MouseEvent) => {
const canvas = canvasRef.value!
const rect = canvas.getBoundingClientRect()
let clientX = 0
let clientY = 0
if (e instanceof TouchEvent) {
const t = e.touches[0] || e.changedTouches[0]
clientX = t.clientX
clientY = t.clientY
} else {
clientX = (e as MouseEvent).clientX
clientY = (e as MouseEvent).clientY
}
return { x: clientX - rect.left, y: clientY - rect.top }
}
const onStart = (e: TouchEvent) => {
drawing.value = true
const p = getPoint(e)
ctx.value!.beginPath()
ctx.value!.moveTo(p.x, p.y)
}
const onMove = (e: TouchEvent) => {
if (!drawing.value) return
const p = getPoint(e)
ctx.value!.lineTo(p.x, p.y)
ctx.value!.stroke()
}
const onEnd = () => {
drawing.value = false
}
const onMouseDown = (e: MouseEvent) => onStart(e as any)
const onMouseMove = (e: MouseEvent) => onMove(e as any)
const onMouseUp = () => onEnd()
const clearCanvas = () => {
resizeCanvas()
}
const dataURLToFile = (dataURL: string, filename: string) => {
const arr = dataURL.split(',')
const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png'
const bstr = atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) u8arr[n] = bstr.charCodeAt(n)
return new File([u8arr], filename, { type: mime })
}
const uploadUrl = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=user_signature`
const requestFullscreen = async (el: HTMLElement) => {
const anyEl = el as any
if (anyEl.requestFullscreen) return anyEl.requestFullscreen()
if (anyEl.webkitRequestFullscreen) return anyEl.webkitRequestFullscreen()
if (anyEl.msRequestFullscreen) return anyEl.msRequestFullscreen()
}
const lockOrientationLandscape = async () => {
const anyScr = screen as any
if (anyScr.orientation && anyScr.orientation.lock) {
try {
await anyScr.orientation.lock('landscape')
} catch {
// ignore
}
}
}
const toggleLandscape = async () => {
landscape.value = !landscape.value
if (landscape.value) {
await requestFullscreen(document.documentElement)
await lockOrientationLandscape()
}
setTimeout(resizeCanvas, 200)
}
const saveSignature = async () => {
if (!userId) {
ElMessage.error('缺少用户信息')
return
}
try {
submitting.value = true
const canvas = canvasRef.value!
const dataUrl = canvas.toDataURL('image/png')
const file = dataURLToFile(dataUrl, `signature_${userId}_${Date.now()}.png`)
const form = new FormData()
form.append('file', file)
const headers: Record<string, string> = { ...(uploadHeaders.value || {}) }
if (effectiveToken) {
headers['Authorization'] = `Bearer ${effectiveToken}`
}
const res = await fetch(uploadUrl, {
method: 'POST',
headers,
body: form
})
const json = await res.json()
if (json.code !== 200) throw new Error(json.message || '上传失败')
// 将签名保存到个人信息(假设 crmuseredit 接受 signature 和 signature_pic_full 字段)
await CrmService.crmuseredit({
id: userId,
signature: json.data.file_name,
signature_pic_full: json.data.file_url
})
ElMessage.success('签名已保存')
// 完成后可提示返回或自动关闭
setTimeout(() => {
if (window.history.length > 1) window.history.back()
}, 800)
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.signature-page {
padding: 16px;
}
.header {
text-align: center;
font-weight: 600;
font-size: 18px;
margin-bottom: 12px;
}
.canvas-wrap {
display: flex;
justify-content: center;
}
.canvas {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 6px;
touch-action: none;
}
.toolbar {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 12px;
}
.tip {
text-align: center;
color: #909399;
margin-top: 8px;
font-size: 12px;
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,149 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="备货单编号" prop="order_no" min-width="140" />
<ElTableColumn label="阿里订单号/PN号" prop="pi_no" min-width="160" />
<ElTableColumn label="客户名称" prop="customer_snapshot" min-width="120" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" min-width="120" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" min-width="100" />
<ElTableColumn label="日期" prop="created_at" min-width="120" />
<ElTableColumn label="补充" prop="remark" min-width="160" />
<ElTableColumn label="状态" prop="status" width="120">
<template #default="scope">
<ElTag type="success">备货完成</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="220" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-button link type="primary" @click="handleView1(scope.row)">查看终版</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 1, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await CrmService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const handleCreate = () => {
// TODO: 跳转到创建备货单页面
router.push('/crmpc/stocking/Add')
}
const handleView1 = (row: any) => {
router.push({
path: '/crmpc/stocking/final_show',
query: { id: row.id } // 可带客户名等参数
})
// TODO: 跳转或弹窗查看备货单
}
</script>
<style scoped lang="scss">
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable { cursor: pointer; }
</style>
@@ -0,0 +1,625 @@
<template>
<div class="stocking-show-page">
<div class="header boxshadow">
<div class="header-left">
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
<div class="sub">
<span class="mr40">备货单编号:{{ header.order_no }}</span>
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain>打印整单</ElButton>
</div>
<!-- 基础信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="阿里订单号/PN号">
<ElInput v-model="base.pn_no" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="日期">
<ElInput v-model="base.date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElFormItem label="客户来源">
<ElInput v-model="base.channel" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户名称">
<ElInput v-model="base.customer_name" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户所在国">
<ElInput v-model="base.client_country" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 订单详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
<div class="items-wrap">
<div class="item-row" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12">
<ElCol :span="8">
<ElFormItem label="车型" label-width="64px">
<ElInput v-model="item.model" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/外" label-width="72px">
<ElInput v-model="item.color_out" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="内饰" label-width="52px">
<ElInput v-model="item.color_in" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="改装" label-width="52px">
<ElInput v-model="item.modify" disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
<template #append>{{ item.unit_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model="item.total_price" disabled>
<template #append>{{ item.total_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
<template #append>{{ item.helper_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
<template #append>{{ item.port_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled>
<template #append>{{ item.profit_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="车架号" label-width="70px">
<ElInput v-model.number="item.vin" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="base.contract_rate" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<!-- 运输详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</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">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const ship = reactive({
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
vin: it?.vin || '',
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/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)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
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 = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page { min-height: 100vh; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 { margin-right: 40px; }
}
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
.form-title .title { margin-left: 6px; }
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.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;
}
</style>
+619
View File
@@ -0,0 +1,619 @@
<template>
<div class="stocking-show-page">
<div class="header boxshadow">
<div class="header-left">
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
<div class="sub">
<span class="mr40">备货单编号:{{ header.order_no }}</span>
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain>打印整单</ElButton>
</div>
<!-- 基础信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="阿里订单号/PN号">
<ElInput v-model="base.pn_no" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="日期">
<ElInput v-model="base.date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElFormItem label="客户来源">
<ElInput v-model="base.channel" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户名称">
<ElInput v-model="base.customer_name" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户所在国">
<ElInput v-model="base.client_country" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 订单详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
<div class="items-wrap">
<div class="item-row" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12">
<ElCol :span="8">
<ElFormItem label="车型" label-width="64px">
<ElInput v-model="item.model" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/外" label-width="72px">
<ElInput v-model="item.color_out" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="内饰" label-width="52px">
<ElInput v-model="item.color_in" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="改装" label-width="52px">
<ElInput v-model="item.modify" disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
<template #append>{{ item.unit_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model="item.total_price" disabled>
<template #append>{{ item.total_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
<template #append>{{ item.helper_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
<template #append>{{ item.port_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled>
<template #append>{{ item.profit_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
</div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="base.contract_rate" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<!-- 运输详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</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">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const ship = reactive({
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/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)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
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 = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page { min-height: 100vh; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 { margin-right: 40px; }
}
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
.form-title .title { margin-left: 6px; }
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.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;
}
</style>
@@ -0,0 +1,157 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="备货单编号" prop="stock_no" min-width="140" />
<ElTableColumn label="阿里订单号/PN号" prop="order_no" min-width="160" />
<ElTableColumn label="客户名称" prop="customer_name" min-width="120" />
<ElTableColumn label="客户来源" prop="customer_source" min-width="120" />
<ElTableColumn label="业务员" prop="salesman" min-width="100" />
<ElTableColumn label="日期" prop="date" min-width="120" />
<ElTableColumn label="补充" prop="remark" min-width="160" />
<ElTableColumn label="状态" prop="status" width="120">
<template #default="scope">
<ElTag type="success">备货完成</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="220" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-button link type="primary" @click="handleView(scope.row)">查看备货单</el-button>
<el-button link type="primary" @click="handleCreateRepair(scope.row)"
>创建终版备货单</el-button
>
<el-button link type="primary" @click="handleView1(scope.row)"
>查看终版备货单</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
// 这里先使用本地假数据占位,等后端接口确定后再切换为真实请求
const mock = Array.from({ length: 8 }).map((_, i) => ({
id: i + 1,
stock_no: `HSAT-JN00${(i + 10).toString().padStart(2, '0')}`,
order_no: 'VCS28762JN2',
customer_name: 'Alicia',
customer_source: '广交会',
salesman: '王玉凯',
date: '2025-08-27',
remark: 'XXXXXXXXXXXX',
status: 1,
}))
tableData.value = mock
pagination.total = 24
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const handleCreate = () => {
// TODO: 跳转到创建备货单页面
router.push('/crmpc/stocking_list/Add')
}
const handleView = (row: any) => {
router.push('/crmpc/stocking_list/show')
// TODO: 跳转或弹窗查看备货单
}
const handleView1 = (row: any) => {
router.push('/crmpc/stocking_list/final_show')
// TODO: 跳转或弹窗查看备货单
}
const handleCreateRepair = (row: any) => {
router.push('/crmpc/stocking_list/final')
// TODO: 跳转创建维修保单
}
</script>
<style scoped lang="scss">
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable { cursor: pointer; }
</style>
@@ -0,0 +1,179 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="备货单编号" prop="order_no" />
<ElTableColumn label="阿里订单号/PN号" prop="pi_no" />
<ElTableColumn label="客户名称" prop="customer_snapshot" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" />
<ElTableColumn label="日期" prop="created_at" />
<ElTableColumn label="补充" prop="remark" />
<!-- <ElTableColumn label="状态" prop="status" width="120">
<template #default="scope">
<ElTag type="success">备货完成</ElTag>
</template>
</ElTableColumn> -->
<ElTableColumn label="操作" fixed="right" min-width="120" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
<el-button
v-if="scope.row.is_use == 0"
link
type="primary"
@click="handleCreateRepair(scope.row)"
>创建终版</el-button
>
<el-button
v-if="scope.row.is_use == 1"
link
type="primary"
@click="handleView1(scope.row)"
>查看终版</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
onMounted(() => {
getList()
})
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
})
const getList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await CrmService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const handleSizeChange = (size: number) => {
pagination.pageSize = size
getList()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
getList()
}
const handleCreate = () => {
// TODO: 跳转到创建备货单页面
router.push('/crmpc/stocking/Add')
}
const handleView = (row: any) => {
router.push({
path: '/crmpc/stocking/show',
query: { id: row.id } // 可带客户名等参数
})
// TODO: 跳转或弹窗查看备货单
}
const handleView1 = (row: any) => {
router.push({
path: '/crmpc/stocking/final_show',
query: { id: row.object_id } // 可带客户名等参数
})
// router.push('/crmpc/stocking/final_show')
// TODO: 跳转或弹窗查看备货单
}
const handleCreateRepair = (row: any) => {
router.push({
path: '/crmpc/stocking/final_add',
query: { id: row.id } // 可带客户名等参数
})
// TODO: 跳转创建维修保单
}
</script>
<style scoped lang="scss">
.stocking-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.stocking-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--art-main-bg-color);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable { cursor: pointer; }
</style>
+264 -540
View File
@@ -12,7 +12,7 @@
<ElForm :model="baseForm" class="base-form">
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="阿里订单号/PN号">
<ElFormItem label="阿里订单号/PI号">
<ElInput v-model="baseForm.pi_no" placeholder="请输入阿里订单号/PN号" />
</ElFormItem>
</ElCol>
@@ -141,12 +141,12 @@
<ElCol :span="4">
<ElFormItem label="颜色/外饰" label-width="72px">
<el-select
v-model="item.color_interior_snapshot"
v-model="item.color_exterior_snapshot"
filterable
placeholder="请选择内容"
@change="
(val) =>
(item.color_interior_code =
(item.color_exterior_code =
(wcolorOptions || []).find((x: any) => x.color_text === val)?.color_code ||
'')
"
@@ -158,18 +158,18 @@
:value="item.color_text"
/>
</el-select>
<ElInput v-model="item.color_interior_code" type="hidden" />
<ElInput v-model="item.color_exterior_code" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/内饰" label-width="72px">
<el-select
v-model="item.color_exterior_snapshot"
v-model="item.color_interior_snapshot"
filterable
placeholder="请选择内容"
@change="
(val) =>
(item.color_exterior_code =
(item.color_interior_code =
(ncolorOptions || []).find((x: any) => x.color_text === val)?.color_code ||
'')
"
@@ -181,7 +181,7 @@
:value="item.color_text"
/>
</el-select>
<ElInput v-model="item.color_exterior_code" type="hidden" />
<ElInput v-model="item.color_interior_code" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="8">
@@ -205,50 +205,15 @@
:value="item.id"
/>
</el-select>
<ElInput v-model="item.modify_list" type="hidden" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model.number="item.retrofit_fee" placeholder="请输入改装费">
<ElFormItem label="单价" label-width="45px">
<ElInput v-model="item.unit_price" placeholder="请输入单价">
<template #append>
<ElSelect v-model="item.modify_fee_currency" style="width: 64px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.modify_fee_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model.number="item.fob_transport_fee" placeholder="请输入FOB段运费">
<template #append>
<ElSelect v-model="item.fob_fee_currency" style="width: 64px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.fob_fee_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model.number="item.unit_price" placeholder="请输入单价">
<template #append>
<ElSelect v-model="item.unit_price_currency" style="width: 64px">
<ElSelect v-model="item.unit_price_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -262,10 +227,10 @@
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model.number="item.total_price" placeholder="请输入总价">
<ElFormItem label="总价" label-width="45px">
<ElInput v-model="item.total_price" placeholder="请输入总价">
<template #append>
<ElSelect v-model="item.total_price_currency" style="width: 64px">
<ElSelect v-model="item.total_price_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -278,11 +243,11 @@
<ElInput v-model="item.total_price_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model.number="item.official_price" placeholder="请输入官方指导价">
<ElCol :span="5">
<ElFormItem label="改装费" label-width="55px">
<ElInput v-model="item.retrofit_fee" placeholder="请输入改装费">
<template #append>
<ElSelect v-model="item.guide_price_currency" style="width: 64px">
<ElSelect v-model="item.modify_fee_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -292,14 +257,14 @@
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.guide_price_currency_snapshot" type="hidden" />
<ElInput v-model="item.modify_fee_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model.number="item.discount_price" placeholder="请输入优惠额">
<ElCol :span="6">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_transport_fee" placeholder="请输入FOB段运费">
<template #append>
<ElSelect v-model="item.discount_currency" style="width: 64px">
<ElSelect v-model="item.fob_fee_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -309,16 +274,14 @@
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.discount_currency_snapshot" type="hidden" />
<ElInput v-model="item.fob_fee_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElCol :span="5">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model.number="item.handling_fee" placeholder="请输入手续费">
<ElInput v-model="item.handling_fee" placeholder="请输入手续费">
<template #append>
<ElSelect v-model="item.helper_fee_currency" style="width: 64px">
<ElSelect v-model="item.helper_fee_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -331,28 +294,13 @@
<ElInput v-model="item.helper_fee_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<!-- <ElCol :span="4">
<ElFormItem label="国内运费" label-width="72px">
<ElInput v-model.number="item.domestic_freight" placeholder="请输入国内运费">
<template #append>
<ElSelect v-model="item.domestic_freight_currency" style="width: 64px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.domestic_freight_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol> -->
<ElCol :span="4">
</ElRow>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model.number="item.port_misc_fee" placeholder="请输入港杂费">
<ElInput v-model="item.port_misc_fee" placeholder="请输入港杂费">
<template #append>
<ElSelect v-model="item.port_fee_currency" style="width: 64px">
<ElSelect v-model="item.port_fee_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -366,12 +314,83 @@
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="利润" label-width="52px">
<ElSelect v-model="item.profit_type" placeholder="请选择" style="width: 100%">
<ElOption label="正在计算利润" value="calc" />
<ElOption label="已确认" value="ok" />
</ElSelect>
<ElInput v-model="item.profit_type_snapshot" type="hidden" />
<ElFormItem label="里程" label-width="52px">
<ElInput v-model="item.mileage" placeholder="请输入里程" />
</ElFormItem>
</ElCol>
<ElCol :span="5" v-if="(item.mileage ?? 0) <= 0">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.official_price" placeholder="请输入官方指导价">
<template #append>
<ElSelect v-model="item.guide_price_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.guide_price_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="5" v-else>
<ElFormItem label="挂牌价" label-width="55px">
<ElInput v-model="item.listing_price" placeholder="请输入挂牌价">
<template #append>
<ElSelect v-model="item.listing_price_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.listing_price_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="6" v-if="(item.mileage ?? 0) <= 0">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount_price" placeholder="请输入优惠额">
<template #append>
<ElSelect v-model="item.discount_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.discount_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="6" v-else>
<ElFormItem label="二手减价" label-width="72px">
<ElInput v-model="item.reduction_price" placeholder="请输入二手减价">
<template #append>
<ElSelect v-model="item.reduction_price_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
<ElInput v-model="item.reduction_price_currency_snapshot" type="hidden" />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="利润" label-width="40px">
<ElInput v-model="item.profit" placeholder="正在计算利润" disabled>
<template #append> <span style="font-size: 11px">美元</span></template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
@@ -390,9 +409,9 @@
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model.number="baseForm.total_cif" placeholder="请输入合计到岸价">
<ElInput v-model="baseForm.total_cif" placeholder="请输入合计到岸价">
<template #append>
<ElSelect v-model="baseForm.total_cif_currency" style="width: 64px">
<ElSelect v-model="baseForm.total_cif_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -407,7 +426,18 @@
</ElCol>
<ElCol :span="4">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="baseForm.contract_exchange_rate" placeholder="请输入" />
<ElInput v-model="baseForm.contract_exchange_rate" placeholder="请输入">
<template #append>
<ElSelect v-model="baseForm.contract_exchange_rate_currency" style="width: 61px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
:label="c.currency_name"
:value="c.currency_code"
/>
</ElSelect>
</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
@@ -445,14 +475,14 @@
@change="
(val) =>
(baseForm.shipping_method_snapshot =
(ncolorOptions || []).find((x: any) => x.id === val)?.item_label || '')
(transportOptions || []).find((x: any) => x.item_value === val)?.item_label || '')
"
>
<ElOption
v-for="t in transportOptions"
:key="t.id"
:key="t.item_value"
:label="t.item_label"
:value="t.id"
:value="t.item_value"
/>
</ElSelect>
<ElInput v-model="baseForm.shipping_method_snapshot" type="hidden" />
@@ -614,7 +644,7 @@
</ElCol>
<ElCol :span="4">
<ElFormItem label="金额">
<ElInput v-model.number="payment.amount" placeholder="请输入金额" />
<ElInput v-model="payment.amount" placeholder="请输入金额" />
</ElFormItem>
</ElCol>
</ElRow>
@@ -635,10 +665,26 @@
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<ElUpload drag action="#" :auto-upload="false" :show-file-list="true" class="upload-box">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">拖拽文件到此处,或点击上传</div>
</ElUpload>
<el-upload
class="upload-box"
:action="actionImageUplode"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleContractUpload"
:before-upload="beforeContractUpload"
accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx,.txt"
>
<div class="qrcode-upload-box">
<img v-if="!contractFile" class="avatar" src="@/assets/img/crm/contract.png" />
<div v-else class="file-preview">
<img v-if="isImageFile(contractFile)" :src="contractFile.url" class="preview-image" />
<div v-else class="file-icon">
<i class="el-icon-document"></i>
<span class="file-name">{{ contractFile.name }}</span>
</div>
</div>
</div>
</el-upload>
</div>
<div class="form-card">
<!-- 底部信息 -->
@@ -751,447 +797,82 @@
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
import { vehicleService } from '@/api/vehicleApi'
import { StringDecoder } from 'string_decoder'
import { useUserStore } from '@/store/modules/user'
import { watch } from 'vue'
import { useStockingForm } from '@/composables/useStockingForm'
import { onMounted } from 'vue'
const { getUserInfo } = useUserStore()
const channelOptions = ref<any>()
const countryOptions = ref<any>()
const modificationOptions = ref<any>()
const currencyOptions = ref<any>()
const accountOptions = ref<any>()
const customerOptions = ref<any>()
const portOptions = ref<any>()
const portOptionsall = ref<any>()
// 外观颜色
const wcolorOptions = ref<Array<any>>([])
// 内饰颜色
const ncolorOptions = ref<Array<any>>([])
//渠道
const getgetChannel = async () => {
const res = await CrmService.getChannel()
channelOptions.value = res || []
}
//客户下拉
const getcustomer = async () => {
const res = await commonService.getCustomerSelect()
customerOptions.value = res || []
}
// 使用公共的备货单表单逻辑
const {
// 数据
baseForm,
items,
paymentItems,
contractFile,
customerDialog,
vehicleDialog,
// 选项数据
channelOptions,
countryOptions,
modificationOptions,
currencyOptions,
accountOptions,
customerOptions,
portOptions,
portOptionsall,
wcolorOptions,
ncolorOptions,
transportOptions,
trade_termsOptions,
// 上传相关
actionImageUplode,
uploadHeaders,
// 方法
initializeForm,
handleSubmit,
getCurrencyRate,
validateContractRate,
wrapMoney,
calcProfit,
// 合同文件方法
isImageFile,
beforeContractUpload,
handleContractUpload,
removeContractFile,
// 订单项方法
copyRow,
addRowAfter,
removeRow,
// 收款项方法
addPaymentItem,
removePaymentItem,
// 客户选择方法
openCustomerDialog,
getCustomerList,
handleCustomerSizeChange,
handleCustomerCurrentChange,
chooseCustomer,
// 车辆选择方法
openVehicleDialog,
getVehicleList,
handleVehicleSizeChange,
handleVehicleCurrentChange,
chooseVehicle
} = useStockingForm(false) // isFinal = false
//国家
const getarea = async () => {
const res = await CrmService.getCountry()
countryOptions.value = res || []
}
//改装项
const getmodification = async () => {
const res = await commonService.getModificationSelect()
modificationOptions.value = res || []
}
//颜色下拉列表
const loadColorOptions = async () => {
try {
const res = await commonService.getColorSelect({ type: 1 })
wcolorOptions.value = res
const response = await commonService.getColorSelect({ type: 2 })
ncolorOptions.value = response
} catch (error) {
console.error('获取颜色列表失败:', error)
ElMessage.error('获取颜色列表失败')
}
}
//账户
const loadAccountOptions = async () => {
try {
const res = await commonService.getAccountSelect()
accountOptions.value = res
} catch (error) {
console.error('获取账户列表失败:', error)
ElMessage.error('获取账户列表失败')
}
}
//币种
const loadCurrencyOptions = async () => {
try {
const res = await commonService.getCurrencySelect()
currencyOptions.value = res
} catch (error) {
console.error('获取币种列表失败:', error)
ElMessage.error('获取币种列表失败')
}
}
//港口
const loadPortOptions = async () => {
try {
const res = await commonService.getPortSelect({ scene: 'domestic' })
const resall = await commonService.getPortSelect({})
portOptions.value = res
portOptionsall.value = resall
} catch (error) {
console.error('获取港口列表失败:', error)
ElMessage.error('获取港口列表失败')
}
}
//字典下拉框
const transportOptions = ref<Array<any>>([])
const trade_termsOptions = ref<Array<any>>([])
const loadDictOptions = async () => {
const response = await commonService.getDictSelect({ scope: '2,15' })
transportOptions.value = response.transport
trade_termsOptions.value = response.trade_terms
}
// 初始化表单
onMounted(async () => {
getcustomer()
await loadCurrencyOptions()
getgetChannel()
getarea()
getmodification()
loadColorOptions()
loadAccountOptions()
loadPortOptions()
loadDictOptions()
// 设置默认汇率
baseForm.contract_exchange_rate = getCurrencyRate('USD')
await initializeForm()
})
const getCurrencyRate = (code: string): string => {
try {
const found = (currencyOptions?.value || []).find((c: any) => c.currency_code === code)
return found?.rate || found?.exchange_rate || ''
} catch (e) {
return ''
}
}
const baseForm = reactive({
pi_no: '',
biz_date: '',
clearance_country_id: '',
clearance_country_snapshot: '',
customer_source_id: '',
customer_source_snapshot: '',
customer_id: '',
customer_snapshot: '',
customer_country_id: '',
customer_country_snapshot: '',
declarant_name: '',
eta_ship_date: '',
shipping_method_id: '',
shipping_method_snapshot: '',
domestic_port_id: '',
domestic_port_snapshot: '',
trade_term_id: '',
trade_term_snapshot: '',
clearance_port_id: '',
clearance_port_snapshot: '',
latest_arrival_date: '',
contract_exchange_rate: '',
remark: '',
total_cif: undefined as number | undefined,
total_cif_currency: 'USD',
total_cif_currency_snapshot: '',
is_final: '0'
})
type ItemRow = {
sku_id: string
model_snapshot: string
color_interior_code: string
color_interior_snapshot: string
color_exterior_code: string
color_exterior_snapshot: string
unit_price: number | undefined
handling_fee: string
fob_transport_fee: number | undefined
port_misc_fee: string
retrofit_fee: string
total_price: number | undefined
official_price: number | undefined
discount_price: number | undefined
fob_fee: number | undefined
fob_fee_currency: string
fob_fee_currency_snapshot: string
helper_fee: number | undefined
helper_fee_currency: string
helper_fee_currency_snapshot: string
domestic_freight: number | undefined
domestic_freight_currency: string
domestic_freight_currency_snapshot: string
port_fee: number | undefined
port_fee_currency: string
port_fee_currency_snapshot: string
unit_price_currency: string
unit_price_currency_snapshot: string
total_price_currency: string
total_price_currency_snapshot: string
guide_price_currency: string
guide_price_currency_snapshot: string
discount: number | undefined
discount_currency: string
discount_currency_snapshot: string
contract_rate: number | undefined
profit_type: string
profit_type_snapshot: string
modify_ids: string[]
modify_list: Array<{ option_id: string; option_name_snapshot: string }>
modify_fee_currency: string
modify_fee_currency_snapshot: string
}
const newItem = (): ItemRow => ({
sku_id: '',
model_snapshot: '',
color_interior_code: '',
color_interior_snapshot: '',
color_exterior_code: '',
color_exterior_snapshot: '',
unit_price: undefined,
handling_fee: '',
fob_transport_fee: undefined,
port_misc_fee: '',
retrofit_fee: '',
total_price: undefined,
official_price: undefined,
discount_price: undefined,
fob_fee: undefined,
fob_fee_currency: 'USD',
fob_fee_currency_snapshot: '',
helper_fee: undefined,
helper_fee_currency: 'USD',
helper_fee_currency_snapshot: '',
domestic_freight: undefined,
domestic_freight_currency: 'USD',
domestic_freight_currency_snapshot: '',
port_fee: undefined,
port_fee_currency: 'USD',
port_fee_currency_snapshot: '',
unit_price_currency: 'USD',
unit_price_currency_snapshot: '',
total_price_currency: 'USD',
total_price_currency_snapshot: '',
guide_price_currency: 'USD',
guide_price_currency_snapshot: '',
discount: undefined,
discount_currency: 'USD',
discount_currency_snapshot: '',
contract_rate: 7.16,
profit_type: 'calc',
profit_type_snapshot: '',
modify_ids: [],
modify_list: [],
modify_fee_currency: 'USD',
modify_fee_currency_snapshot: ''
})
const items = ref<ItemRow[]>([newItem()])
const copyRow = (idx: number) => {
const copied = JSON.parse(JSON.stringify(items.value[idx])) as ItemRow
copied.uid = Math.random().toString(36).slice(2)
items.value.splice(idx + 1, 0, copied)
}
const addRowAfter = (idx: number) => {
items.value.splice(idx + 1, 0, newItem())
}
const removeRow = (idx: number) => {
items.value.splice(idx, 1)
}
// 监听币种变化,自动更新汇率
watch(
() => items.value[0].unit_price_currency,
(newCurrency) => {
if (newCurrency) {
baseForm.contract_exchange_rate = getCurrencyRate(newCurrency)
}
}
)
type PaymentItem = {
remit_country_id: string
remit_country_name_snapshot: string
remit_date: string
currency_code: string
currency_name_snapshot: string
account_id: string
account_name_snapshot: string
remitter_name: string
amount: number | undefined
}
const newPaymentItem = (): PaymentItem => ({
remit_country_id: '',
remit_country_name_snapshot: '',
remit_date: '',
currency_code: '',
currency_name_snapshot: '',
account_id: '',
account_name_snapshot: '',
remitter_name: '',
amount: undefined
})
const paymentItems = ref<PaymentItem[]>([newPaymentItem()])
const addPaymentItem = () => {
paymentItems.value.push(newPaymentItem())
}
const removePaymentItem = (idx: number) => {
if (paymentItems.value.length > 1) {
paymentItems.value.splice(idx, 1)
}
}
const wrapMoney = (amount: any, currency: string, remark: string) => ({
currency: currency || '',
amount: amount === undefined || amount === null ? '' : String(amount),
rate: getCurrencyRate(currency),
remark
})
const handleSubmit = async () => {
const mappedItems = items.value.map((it) => ({
sku_id: it.sku_id,
model_snapshot: it.model_snapshot,
color_interior_code: it.color_interior_code,
color_interior_snapshot: it.color_interior_snapshot,
color_exterior_code: it.color_exterior_code,
color_exterior_snapshot: it.color_exterior_snapshot,
unit_price: wrapMoney(it.unit_price, (it as any).unit_price_currency, '单价'),
handling_fee: wrapMoney(it.handling_fee, (it as any).helper_fee_currency, '手续费'),
fob_transport_fee: wrapMoney(
it.fob_transport_fee ?? (it as any).fob_fee,
(it as any).fob_fee_currency,
'FOB运输费'
),
port_misc_fee: wrapMoney(
it.port_misc_fee ?? (it as any).port_fee,
(it as any).port_fee_currency,
'港杂费'
),
retrofit_fee: wrapMoney((it as any).retrofit_fee, (it as any).modify_fee_currency, '单价'),
modify_list: it.modify_list,
total_price: wrapMoney(it.total_price, (it as any).total_price_currency, '总价'),
official_price: wrapMoney(
(it as any).official_price ?? (it as any).guide_price,
(it as any).guide_price_currency,
'官方指导价'
),
discount_price: wrapMoney(
(it as any).discount_price ?? (it as any).discount,
(it as any).discount_currency,
'优惠额'
)
}))
const payload = {
...baseForm,
veh_list: mappedItems,
account_list: paymentItems.value
}
console.log('submit payload', payload)
// const { message } = await CrmService.addStock(payload)
// ElMessage.success(message)
}
// 选择客户弹框逻辑
const customerDialog = reactive({
visible: false,
search: { keyword: '' },
loading: false,
tableData: [] as any[],
pagination: { currentPage: 1, pageSize: 10, total: 0 }
})
const openCustomerDialog = () => {
customerDialog.visible = true
customerDialog.pagination.currentPage = 1
getCustomerList()
}
const getCustomerList = async () => {
customerDialog.loading = true
try {
const { currentPage, pageSize } = customerDialog.pagination
const params = { current: currentPage, size: pageSize, keyword: customerDialog.search.keyword }
const { data, current, size, total } = await CrmService.getcustomerList(params as any)
customerDialog.tableData = data || []
Object.assign(customerDialog.pagination, { currentPage: current, pageSize: size, total })
} catch (e) {
} finally {
customerDialog.loading = false
}
}
const handleCustomerSizeChange = (size: number) => {
customerDialog.pagination.pageSize = size
getCustomerList()
}
const handleCustomerCurrentChange = (page: number) => {
customerDialog.pagination.currentPage = page
getCustomerList()
}
const chooseCustomer = (row: any) => {
baseForm.customer_id = row.id || ''
baseForm.customer_snapshot = row.contacts || row.name || ''
customerDialog.visible = false
}
// 选择车型弹框逻辑
const vehicleDialog = reactive({
visible: false,
targetIndex: -1,
search: { keyword: '' },
loading: false,
tableData: [] as any[],
pagination: { currentPage: 1, pageSize: 10, total: 0 }
})
const openVehicleDialog = (index: number) => {
vehicleDialog.targetIndex = index
vehicleDialog.visible = true
vehicleDialog.pagination.currentPage = 1
getVehicleList()
}
const getVehicleList = async () => {
vehicleDialog.loading = true
try {
const { currentPage, pageSize } = vehicleDialog.pagination
const params = { current: currentPage, size: pageSize, keyword: vehicleDialog.search.keyword }
const { records, current, size, total } = await vehicleService.getVehicleList(params)
vehicleDialog.tableData = records || []
Object.assign(vehicleDialog.pagination, { currentPage: current, pageSize: size, total })
} catch (e) {
ElMessage.error('获取车辆列表失败')
} finally {
vehicleDialog.loading = false
}
}
const handleVehicleSizeChange = (size: number) => {
vehicleDialog.pagination.pageSize = size
getVehicleList()
}
const handleVehicleCurrentChange = (page: number) => {
vehicleDialog.pagination.currentPage = page
getVehicleList()
}
const chooseVehicle = (row: any) => {
const idx = vehicleDialog.targetIndex
if (idx >= 0 && idx < items.value.length) {
const target = items.value[idx]
target.sku_id = row.id || ''
target.model_snapshot = row.model_name || row.name || ''
// 回显颜色与指导价
target.color_exterior_code = row.color_exterior_code || ''
target.color_exterior_snapshot = row.color_exterior_text || ''
target.color_interior_code = row.color_interior_code || ''
target.color_interior_snapshot = row.color_interior_text || ''
target.official_price = row.msrp ?? target.official_price
if (row.currency) {
target.official_price_currency = row.currency
}
// 可按需带出更多字段回显
}
vehicleDialog.visible = false
}
</script>
<style scoped lang="scss">
@@ -1199,7 +880,7 @@
padding: 4px;
}
:deep(.el-select__selected-item span) {
font-size: 12px;
font-size: 11px;
}
.form-title {
font-weight: bold;
@@ -1210,7 +891,6 @@
margin-left: 6px;
}
.stocking-add-page {
// background: var(--art-main-bg-color);
min-height: 100vh;
}
@@ -1273,5 +953,49 @@
.payment-item {
margin-bottom: 12px;
}
.upload-box { width: 200px; }
</style>
.upload-box {
width: 200px;
}
.file-preview {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.file-icon {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: #f5f7fa;
border-radius: 4px;
border: 1px dashed #d9d9d9;
}
.file-icon i {
font-size: 32px;
color: #409eff;
margin-bottom: 8px;
}
.file-name {
font-size: 12px;
color: #606266;
text-align: center;
word-break: break-all;
padding: 0 8px;
}
</style>
File diff suppressed because it is too large Load Diff
+32 -40
View File
@@ -32,12 +32,12 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="备货单编号" prop="stock_no" min-width="140" />
<ElTableColumn label="阿里订单号/PN号" prop="order_no" min-width="160" />
<ElTableColumn label="客户名称" prop="customer_name" min-width="120" />
<ElTableColumn label="客户来源" prop="customer_source" min-width="120" />
<ElTableColumn label="业务员" prop="salesman" min-width="100" />
<ElTableColumn label="日期" prop="date" min-width="120" />
<ElTableColumn label="备货单编号" prop="order_no" min-width="140" />
<ElTableColumn label="阿里订单号/PN号" prop="pi_no" min-width="160" />
<ElTableColumn label="客户名称" prop="customer_snapshot" min-width="120" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" min-width="120" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" min-width="100" />
<ElTableColumn label="日期" prop="created_at" min-width="120" />
<ElTableColumn label="补充" prop="remark" min-width="160" />
<ElTableColumn label="状态" prop="status" width="120">
<template #default="scope">
@@ -47,13 +47,7 @@
<ElTableColumn label="操作" fixed="right" min-width="220" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-button link type="primary" @click="handleView(scope.row)">查看备货单</el-button>
<el-button link type="primary" @click="handleCreateRepair(scope.row)"
>创建终版备货单</el-button
>
<el-button link type="primary" @click="handleView1(scope.row)"
>查看终版备货单</el-button
>
<el-button link type="primary" @click="handleView1(scope.row)">查看终版</el-button>
</ElRow>
</template>
</ElTableColumn>
@@ -64,7 +58,12 @@
</template>
<script setup lang="ts">
import { router } from '@/router'
import { router } from '@/router'
import { CrmService } from '@/api/crmApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
@@ -84,20 +83,19 @@ import { router } from '@/router'
const getList = async () => {
loading.value = true
try {
// 这里先使用本地假数据占位,等后端接口确定后再切换为真实请求
const mock = Array.from({ length: 8 }).map((_, i) => ({
id: i + 1,
stock_no: `HSAT-JN00${(i + 10).toString().padStart(2, '0')}`,
order_no: 'VCS28762JN2',
customer_name: 'Alicia',
customer_source: '广交会',
salesman: '王玉凯',
date: '2025-08-27',
remark: 'XXXXXXXXXXXX',
status: 1,
}))
tableData.value = mock
pagination.total = 24
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 1, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await CrmService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
@@ -114,20 +112,15 @@ import { router } from '@/router'
const handleCreate = () => {
// TODO: 跳转到创建备货单页面
router.push('/crmpc/stocking_list/Add')
}
const handleView = (row: any) => {
router.push('/crmpc/stocking_list/show')
// TODO: 跳转或弹窗查看备货单
router.push('/crmpc/stocking/Add')
}
const handleView1 = (row: any) => {
router.push('/crmpc/stocking_list/final_show')
router.push({
path: '/crmpc/stocking/final_show',
query: { id: row.id } // 可带客户名等参数
})
// TODO: 跳转或弹窗查看备货单
}
const handleCreateRepair = (row: any) => {
router.push('/crmpc/stocking_list/final')
// TODO: 跳转创建维修保单
}
</script>
<style scoped lang="scss">
@@ -153,5 +146,4 @@ import { router } from '@/router'
align-items: center;
}
.clickable { cursor: pointer; }
</style>
</style>
File diff suppressed because it is too large Load Diff
+474 -123
View File
@@ -8,7 +8,7 @@
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain>打印整单</ElButton>
<ElButton type="primary" plain @click="printCurrent">打印整单</ElButton>
</div>
<!-- 基础信息 -->
@@ -26,7 +26,7 @@
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="关闭日期">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
@@ -77,20 +77,6 @@
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
@@ -106,22 +92,19 @@
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
@@ -129,13 +112,6 @@
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="国内运费" label-width="72px">
<ElInput v-model="item.domestic_freight" disabled>
<template #append>{{ item.domestic_freight_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
@@ -143,7 +119,47 @@
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="里程" label-width="52px">
<ElInput v-model="item.mileage" disabled />
</ElFormItem>
</ElCol>
<template v-if="(item.mileage ?? 0) <= 0">
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<template v-else>
<ElCol :span="4">
<ElFormItem label="挂牌价" label-width="55px">
<ElInput v-model="item.listing_price" disabled>
<template #append>{{ item.listing_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="二手减价" label-width="72px">
<ElInput v-model="item.reduction_price" disabled>
<template #append>{{ item.reduction_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</template>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled>
<template #append>{{ item.profit_currency }}</template>
@@ -174,114 +190,364 @@
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="国内收货港"><ElInput v-model="ship.domestic_port" disabled /></ElFormItem></ElCol>
<ElCol :span="8"><ElFormItem label="贸易条款/清关目的港"><ElInput v-model="ship.terms" disabled /></ElFormItem></ElCol>
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"><ElFormItem label="最迟到港时间"><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem></ElCol>
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">计划收款信息</span></div>
<ElRow :gutter="12">
<ElCol :span="4"><ElFormItem label="汇款国"><ElInput v-model="pay.remit_country" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="日期"><ElInput v-model="pay.date" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="币种"><ElInput v-model="pay.currency" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="账户"><ElInput v-model="pay.account" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="汇款人"><ElInput v-model="pay.payer" disabled /></ElFormItem></ElCol>
<ElCol :span="4"><ElFormItem label="金额"><ElInput v-model="pay.amount" disabled /></ElFormItem></ElCol>
</ElRow>
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</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">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog v-model="pdf.visible" title="打印预览" width="80%" top="40px" :close-on-click-modal="false">
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
<div style="display:flex; justify-content:flex-end; gap: 8px;">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex:1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
const header = reactive({ order_no: 'HSAT-JN00010', salesman: '王玉杰' })
import { CrmService } from '@/api/crmApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
const printCurrent = async () => {
const res = await CrmService.printStock({ id: stocking_id })
openWithResponse(res)
preparePreview()
}
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: 'VCSC250827-JN1',
date: '2025-08-31',
pn_no: '',
date: '',
close_date: '',
channel: '广交会',
customer_name: 'Alicia',
client_country: '埃赫',
total_cif: '60,652.00',
total_cif_currency: '美元',
contract_rate: '7.16'
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref([
{
model: '丰田卡罗拉 2024款 2.0L CVT 精英版',
color_out: '银色',
color_in: '黑',
modify: '改装仿皮座椅',
modify_fee: '1000',
modify_fee_currency: '美元',
fob_fee: '500',
fob_fee_currency: '美元',
unit_price: '14,545.00',
unit_price_currency: '美元',
total_price: '14,545.00',
total_price_currency: '美元',
guide_price: '139800',
guide_price_currency: '人民币',
discount: '42000',
discount_currency: '人民币',
helper_fee: '1500',
helper_fee_currency: '美元',
domestic_freight: '1500',
domestic_freight_currency: '美元',
port_fee: '600',
port_fee_currency: '美元',
profit: '1000',
profit_currency: '美元'
},
{
model: '丰田卡罗拉 2024款 2.0L CVT 精英版',
color_out: '黑色',
color_in: '黑',
modify: '改装仿皮座椅',
modify_fee: '1000',
modify_fee_currency: '美元',
fob_fee: '500',
fob_fee_currency: '美元',
unit_price: '14,545.00',
unit_price_currency: '美元',
total_price: '14,545.00',
total_price_currency: '美元',
guide_price: '139800',
guide_price_currency: '人民币',
discount: '42000',
discount_currency: '人民币',
helper_fee: '1500',
helper_fee_currency: '美元',
domestic_freight: '1500',
domestic_freight_currency: '美元',
port_fee: '600',
port_fee_currency: '美元',
profit: '1000',
profit_currency: '美元'
}
])
const items = ref<any[]>([])
const ship = reactive({
customs: '陌生人',
plan: '2025-08-28',
method: '滚装',
domestic_port: '上海',
terms: '贸易条款展示字段',
latest_arrival: '2025-09-03'
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
const pay = reactive({
remit_country: '汇款国',
date: '2025-08-28',
currency: '美元',
account: '中行1451',
payer: 'AMOTOR TECHNOLOGY CO., LIMITED/XTRANSFER LIMITED',
amount: '15,000.00'
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
mileage: it?.mileage ?? 0,
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
listing_price: it?.listing_price?.amount ?? '',
listing_price_currency: it?.listing_price?.currency ?? '',
reduction_price: it?.reduction_price?.amount ?? '',
reduction_price_currency: it?.reduction_price?.currency ?? '',
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/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)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
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 = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
@@ -313,6 +579,91 @@
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.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;
}
</style>
+18 -16
View File
@@ -2,7 +2,7 @@
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
<!-- <ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton> -->
</div>
<div class="toolbar-right">
<el-input
@@ -64,8 +64,11 @@
</template>
<script setup lang="ts">
import { router } from '@/router'
import { router } from '@/router'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
@@ -84,20 +87,19 @@ import { router } from '@/router'
const getList = async () => {
loading.value = true
try {
// 这里先使用本地假数据占位,等后端接口确定后再切换为真实请求
const mock = Array.from({ length: 8 }).map((_, i) => ({
id: i + 1,
stock_no: `HSAT-JN00${(i + 10).toString().padStart(2, '0')}`,
order_no: 'VCS28762JN2',
customer_name: 'Alicia',
customer_source: '广交会',
salesman: '王玉凯',
date: '2025-08-27',
remark: 'XXXXXXXXXXXX',
status: 1,
}))
tableData.value = mock
pagination.total = 24
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await manageService.getstocklist(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
@@ -0,0 +1,625 @@
<template>
<div class="stocking-show-page">
<div class="header boxshadow">
<div class="header-left">
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
<div class="sub">
<span class="mr40">备货单编号:{{ header.order_no }}</span>
<span>业务员:{{ header.salesman }}</span>
</div>
</div>
<ElButton type="primary" plain>打印整单</ElButton>
</div>
<!-- 基础信息 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="阿里订单号/PN号">
<ElInput v-model="base.pn_no" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="日期">
<ElInput v-model="base.date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="清关目的国">
<ElInput v-model="base.close_date" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="3">
<ElFormItem label="客户来源">
<ElInput v-model="base.channel" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户名称">
<ElInput v-model="base.customer_name" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="客户所在国">
<ElInput v-model="base.client_country" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<!-- 订单详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
<div class="items-wrap">
<div class="item-row" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12">
<ElCol :span="8">
<ElFormItem label="车型" label-width="64px">
<ElInput v-model="item.model" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="颜色/外" label-width="72px">
<ElInput v-model="item.color_out" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="内饰" label-width="52px">
<ElInput v-model="item.color_in" disabled />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="改装" label-width="52px">
<ElInput v-model="item.modify" disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="单价" label-width="52px">
<ElInput v-model="item.unit_price" disabled>
<template #append>{{ item.unit_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="总价" label-width="52px">
<ElInput v-model="item.total_price" disabled>
<template #append>{{ item.total_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="改装费" label-width="60px">
<ElInput v-model="item.modify_fee" disabled>
<template #append>{{ item.modify_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="FOB段运费" label-width="84px">
<ElInput v-model="item.fob_fee" disabled>
<template #append>{{ item.fob_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="官方指导价" label-width="85px">
<ElInput v-model="item.guide_price" disabled>
<template #append>{{ item.guide_price_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="优惠" label-width="52px">
<ElInput v-model="item.discount" disabled>
<template #append>{{ item.discount_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="4">
<ElFormItem label="手续费" label-width="60px">
<ElInput v-model="item.helper_fee" disabled>
<template #append>{{ item.helper_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="港杂费" label-width="60px">
<ElInput v-model="item.port_fee" disabled>
<template #append>{{ item.port_fee_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="利润" label-width="52px">
<ElInput v-model="item.profit" disabled>
<template #append>{{ item.profit_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="车架号" label-width="70px">
<ElInput v-model.number="item.vin" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
<ElRow :gutter="12">
<ElCol :span="5">
<ElFormItem label="合计到岸价" label-width="85px">
<ElInput v-model="base.total_cif" disabled>
<template #append>{{ base.total_cif_currency }}</template>
</ElInput>
</ElFormItem>
</ElCol>
<ElCol :span="4">
<ElFormItem label="合同汇率" label-width="70px">
<ElInput v-model="base.contract_rate" disabled />
</ElFormItem>
</ElCol>
</ElRow>
</div>
</div>
<!-- 运输详情 -->
<div class="boxshadow">
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="国内收货港"
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
></ElCol>
<ElCol :span="8"
><ElFormItem label="贸易条款/清关目的港"
><ElInput v-model="ship.terms" disabled /></ElFormItem
></ElCol>
</ElRow>
<ElRow :gutter="12">
<ElCol :span="6"
><ElFormItem label="最迟到港时间"
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
<!-- 计划收款信息 -->
<div class="boxshadow">
<div class="form-title"
><span class="border"></span><span class="title">计划收款信息</span></div
>
<div v-for="(p, idx) in payments" :key="idx">
<ElRow :gutter="12">
<ElCol :span="4"
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
></ElCol>
<ElCol :span="4"
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
></ElCol>
</ElRow>
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
<div v-if="isImageFile(contractFile)" class="image-preview">
<img
:src="contractFile.url"
:alt="contractFile.name"
class="preview-image"
@click="previewImage(contractFile)"
/>
<div class="file-info">
<span class="file-name">{{ contractFile.name }}</span>
<el-button type="text" size="small" @click="previewImage(contractFile)">
预览
</el-button>
</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">{{ contractFile.name }}</span>
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
下载
</el-button>
</div>
</div>
</div>
<div v-else class="no-file">
<i class="iconfont-sys">&#xe60c;</i>
<span>暂无合同文件</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
const route = useRoute()
const stocking_id = route.query.id
const stockingInfo = ref<any>({})
// 回显视图模型,字段与模板一致
const header = reactive({ order_no: '', salesman: '' })
const base = reactive({
pn_no: '',
date: '',
close_date: '',
channel: '',
customer_name: '',
client_country: '',
total_cif: '',
total_cif_currency: '',
contract_rate: ''
})
const items = ref<any[]>([])
const ship = reactive({
customs: '',
plan: '',
method: '',
domestic_port: '',
terms: '',
latest_arrival: ''
})
const payments = ref<any[]>([])
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
const toMoneyView = (m: any) => ({
amount: m?.amount ?? '',
currency: m?.currency ?? ''
})
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.getStockDetail({ id: stocking_id })
stockingInfo.value = res
// 头部
header.order_no = res?.order_no || res?.pi_no || ''
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
// 基础信息
base.pn_no = res?.pi_no || ''
base.date = res?.biz_date || ''
base.close_date = res?.clearance_country_snapshot || ''
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
// 订单明细
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
items.value = list.map((it: any) => {
const unit = toMoneyView(it?.unit_price)
const total = toMoneyView(it?.total_price)
const guide = toMoneyView(it?.official_price)
const discount = toMoneyView(it?.discount_price)
const helper = toMoneyView(it?.handling_fee)
const fob = toMoneyView(it?.fob_transport_fee)
const port = toMoneyView(it?.port_misc_fee)
const profit = toMoneyView(it?.profit)
return {
model: it?.model_snapshot || '',
vin: it?.vin || '',
color_out: it?.color_exterior_snapshot || '',
color_in: it?.color_interior_snapshot || '',
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
.map((m: any) => m?.option_name_snapshot)
.filter(Boolean)
.join('、'),
modify_fee: it?.retrofit_fee?.amount ?? '',
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
fob_fee: fob.amount,
fob_fee_currency: fob.currency,
unit_price: unit.amount,
unit_price_currency: unit.currency,
total_price: total.amount,
total_price_currency: total.currency,
guide_price: guide.amount,
guide_price_currency: guide.currency,
discount: discount.amount,
discount_currency: discount.currency,
helper_fee: helper.amount,
helper_fee_currency: helper.currency,
domestic_freight: it?.domestic_freight?.amount ?? '',
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
port_fee: port.amount,
port_fee_currency: port.currency,
profit: profit.amount,
profit_currency: profit.currency
}
})
// 运输详情
ship.customs = res?.declarant_name || ''
ship.plan = res?.eta_ship_date || ''
ship.method = res?.shipping_method_snapshot || ''
ship.domestic_port = res?.domestic_port_snapshot || ''
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
/^\s*\/\s*$/,
''
)
ship.latest_arrival = res?.latest_arrival_date || ''
// 合计与汇率
base.total_cif = res?.cif_total_amount?.amount ?? ''
base.total_cif_currency = res?.cif_total_amount?.currency || ''
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
// 计划收款(全部展示)
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
remit_country: p?.remit_country_name_snapshot || '',
date: p?.remit_date || '',
currency: p?.currency_name_snapshot || p?.currency_code || '',
account: p?.account_name_snapshot || '',
payer: p?.remitter_name || '',
amount: p?.amount ?? ''
}))
// 合同文件
if (res?.contract) {
contractFile.value = {
name: '合同文件',
url: res.contract_url || res.contract,
file_name: res.contract
}
}
}
// 判断是否为图片文件
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
// 检查文件名
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/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)$/i.test(urlWithoutParams)) {
return true
}
}
return false
}
// 预览图片
const previewImage = (file: { name: string; url: string; file_name: string }) => {
// 创建图片预览弹窗
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 = (file: { name: string; url: string; file_name: string }) => {
try {
const link = document.createElement('a')
link.href = file.url
link.download = file.name
link.target = '_blank'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} catch (error) {
console.error('下载失败:', error)
ElMessage.error('下载失败,请稍后重试')
}
}
onMounted(() => {
getdetilfn()
})
</script>
<style scoped lang="scss">
.stocking-show-page { min-height: 100vh; }
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
color: #333333;
.header-left {
flex: 1;
text-align: center;
}
.title {
font-size: 18px;
font-weight: 600;
margin-bottom: 15px;
}
.sub {
color: #333333;
}
.mr40 { margin-right: 40px; }
}
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
.form-title .title { margin-left: 6px; }
.items-wrap { margin-top: 6px; }
.item-row { padding: 8px 0; }
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
.file-preview {
padding: 20px;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.contract-file {
width: 100%;
max-width: 400px;
}
.image-preview {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.preview-image {
max-width: 200px;
max-height: 200px;
object-fit: contain;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: transform 0.2s ease;
}
.preview-image:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.document-preview {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background: #fafafa;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: #409eff;
border-radius: 8px;
color: white;
font-size: 24px;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.file-name {
font-size: 14px;
color: #333;
word-break: break-all;
}
.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;
}
</style>
+103 -33
View File
@@ -32,39 +32,86 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="备货单编号" prop="stock_no" min-width="140" />
<ElTableColumn label="阿里订单号/PN号" prop="order_no" min-width="160" />
<ElTableColumn label="客户名称" prop="customer_name" min-width="120" />
<ElTableColumn label="客户来源" prop="customer_source" min-width="120" />
<ElTableColumn label="业务员" prop="salesman" min-width="100" />
<ElTableColumn label="日期" prop="date" min-width="120" />
<ElTableColumn label="补充" prop="remark" min-width="160" />
<ElTableColumn label="状态" prop="status" width="120">
<ElTableColumn label="备货单编号" prop="order_no" />
<ElTableColumn label="阿里订单号/PN号" prop="pi_no" />
<ElTableColumn label="客户名称" prop="customer_snapshot" />
<ElTableColumn label="客户来源" prop="customer_source_snapshot" />
<ElTableColumn label="业务员" prop="salesperson_snapshot" />
<ElTableColumn label="日期" prop="created_at" />
<ElTableColumn label="补充" prop="remark" />
<!-- <ElTableColumn label="状态" prop="status" width="120">
<template #default="scope">
<ElTag type="success">备货完成</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="操作" fixed="right" min-width="220" align="center">
</ElTableColumn> -->
<ElTableColumn label="操作" fixed="right" min-width="120" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
<el-button link type="primary" @click="handleView(scope.row)">查看备货单</el-button>
<el-button link type="primary" @click="handleCreateRepair(scope.row)"
>创建终版备货单</el-button
<el-link
type="warning"
underline="never"
@click="printfn(scope.row)"
style="margin-right: 8px"
>
<el-button link type="primary" @click="handleView1(scope.row)"
>查看终版备货单</el-button
<i class="iconfont-sys">&#xe782;</i> 打印
</el-link>
<el-link
type="success"
underline="never"
@click="handleView(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe76c;</i> 查看
</el-link>
<el-link
v-if="scope.row.is_use == 0"
type="primary"
underline="never"
@click="handleCreateRepair(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 创建终版
</el-link>
<el-link
v-if="scope.row.is_use == 1"
type="success"
underline="never"
@click="handleView1(scope.row)"
>
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<!-- PDF 预览弹窗 -->
<el-dialog v-model="pdf.visible" title="打印预览" width="80%" top="40px" :close-on-click-modal="false">
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
<div style="display:flex; justify-content:flex-end; gap: 8px;">
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
<el-button @click="download">下载</el-button>
</div>
<iframe
v-if="pdf.blobUrl || pdf.url"
ref="frameRef"
:src="pdf.blobUrl || pdf.url"
style="flex:1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
import { router } from '@/router'
import { CrmService } from '@/api/crmApi'
import { usePdfPreview } from '@/composables/usePdfPreview'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
@@ -72,6 +119,9 @@ import { router } from '@/router'
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
// PDF 预览/打印(公共)
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
onMounted(() => {
getList()
})
@@ -84,20 +134,19 @@ import { router } from '@/router'
const getList = async () => {
loading.value = true
try {
// 这里先使用本地假数据占位,等后端接口确定后再切换为真实请求
const mock = Array.from({ length: 8 }).map((_, i) => ({
id: i + 1,
stock_no: `HSAT-JN00${(i + 10).toString().padStart(2, '0')}`,
order_no: 'VCS28762JN2',
customer_name: 'Alicia',
customer_source: '广交会',
salesman: '王玉凯',
date: '2025-08-27',
remark: 'XXXXXXXXXXXX',
status: 1,
}))
tableData.value = mock
pagination.total = 24
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
is_final: 0, // 只获取备货单,非终版
keyword: keyword.value
}
const { records, current, size, total } = await CrmService.getStockList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
@@ -117,17 +166,38 @@ import { router } from '@/router'
router.push('/crmpc/stocking/Add')
}
const handleView = (row: any) => {
router.push('/crmpc/stocking/show')
router.push({
path: '/crmpc/stocking/show',
query: { id: row.id } // 可带客户名等参数
})
// TODO: 跳转或弹窗查看备货单
}
const handleView1 = (row: any) => {
router.push('/crmpc/stocking/final_show')
router.push({
path: '/crmpc/stocking/final_show',
query: { id: row.object_id } // 可带客户名等参数
})
// router.push('/crmpc/stocking/final_show')
// TODO: 跳转或弹窗查看备货单
}
const handleCreateRepair = (row: any) => {
router.push('/crmpc/stocking/final_add')
router.push({
path: '/crmpc/stocking/final_add',
query: { id: row.id } // 可带客户名等参数
})
// TODO: 跳转创建维修保单
}
//打印
const printfn = (row: any) => {
CrmService.printStock({ id: row.id })
.then((res) => {
openWithResponse(res)
preparePreview()
})
.catch((err) => {
console.log(err)
})
}
</script>
<style scoped lang="scss">