feat(CoilSelector): 新增入场卷号字段并调整当前卷号显示

feat(customer): 新增客户相关配卷和财务信息查询接口

fix(base.vue): 修复发货单时间条件显示问题

refactor(CustomerEdit): 替换地址选择组件为普通输入框

feat(CoilSelector): 增加入场卷号查询条件并调整对话框宽度

style(OrderEdit): 调整客户名称和销售员选择框宽度

refactor(ChinaAreaSelect): 优化地址解析逻辑并支持空对象处理

feat(FileUpload/FileList): 新增文件预览功能组件

refactor(KLPService/CustomerSelect): 优化客户选择组件并支持自定义字段绑定

fix(AbnormalForm): 修复异常位置校验逻辑并保留当前卷号

feat(ContractTabs): 新增合同附件展示功能

refactor(warehouse/record): 重构操作记录统计展示方式

feat(contract): 集成客户选择组件并优化合同信息填充

refactor(order): 调整订单表单布局并集成合同信息

feat(FilePreview): 新增文件预览组件

feat(customer): 新增财务状态和发货配卷展示

refactor(CustomerOrder): 移除冗余代码并优化布局

feat(PlanDetailForm): 新增合同附件查看功能

feat(dict): 新增字典管理页面
This commit is contained in:
砂糖
2026-04-06 13:16:45 +08:00
parent 4075ead84e
commit 1fa4c55869
21 changed files with 1158 additions and 192 deletions

View File

@@ -47,12 +47,17 @@ function formatAreaText(value) {
return { standard, custom };
}
// 非组合格式(纯标准地址/纯自定义地址)→ 默认归为standard
return { standard: trimVal, custom: '' };
// 非组合格式(纯任意字符串)→ 归为custom
return { standard: '', custom: trimVal };
}
// ========== 场景3输入是对象 → 格式化为组合字符串 ==========
if (typeof value === 'object' && !Array.isArray(value)) {
// 处理空对象
if (Object.keys(value).length === 0) {
return '[]()';
}
const { standard = '', custom = '' } = value;
// 转字符串并去空格
const standardStr = String(standard).trim();
@@ -71,11 +76,19 @@ function formatAreaText(value) {
*/
function formatAreaTextEnhanced(value, keyType = 'name') {
// 先调用基础版解析/格式化
const result = formatAreaText(value);
let result = formatAreaText(value);
// 如果是解析模式返回对象且keyType为name转换code为name
// 如果是解析模式返回对象且keyType为name检查是否为代码输入
if (typeof result === 'object' && keyType === 'name') {
result.standard = convertCodeToName(result.standard);
// 检查custom是否可能是代码不包含中文
if (result.custom && !/[\u4e00-\u9fa5]/.test(result.custom)) {
// 尝试转换code为name
const convertedName = convertCodeToName(result.custom);
// 如果转换成功返回非空字符串则将其移到standard字段
if (convertedName) {
result = { standard: convertedName, custom: '' };
}
}
}
return result;

View File

@@ -26,6 +26,7 @@
<script>
import areaData from './data.js'
import { formatAreaTextEnhanced } from './index.js'
export default {
name: 'ChinaAreaSelectWithDetail',
@@ -105,32 +106,9 @@ export default {
* @param {String} val - 外部传入的「[标准地址](自定义地址)」格式值
*/
parseCombineValue(val) {
if (!val) {
this.areaValue = []
this.detailAddress = ''
return
}
// 核心正则:匹配「[xxx](yyy)」格式分组提取xxx和yyy无匹配则为空
const reg = /\[([^\]]*)\]\(([^)]*)\)/
const matchResult = val.match(reg)
// 提取标准地址(第一个分组)和自定义地址(第二个分组)
const standardAddress = matchResult?.[1] || ''
const customAddress = matchResult?.[2] || ''
// 1. 处理自定义地址:直接赋值
this.detailAddress = customAddress.trim()
// 2. 处理标准地址转换为区域选择器的code数组
if (standardAddress) {
const standardArr = standardAddress.split('/').filter(Boolean)
this.areaValue = this.keyType === 'name'
? this.convertNameToCode(standardArr) // name转code
: standardArr.filter(code => !!areaData[code]) // code直接过滤无效值
} else {
this.areaValue = []
}
const formattedAddress = formatAreaTextEnhanced(val)
this.areaValue = formattedAddress.standard.split('/').filter(Boolean)
this.detailAddress = formattedAddress.custom.trim()
},
/**

View File

@@ -1,6 +1,12 @@
export const defaultColumns = [
{
label: '卷号',
label: '入场卷号',
align: 'center',
prop: 'enterCoilNo',
showOverflowTooltip: true
},
{
label: '当前卷号',
align: 'center',
prop: 'currentCoilNo',
showOverflowTooltip: true

View File

@@ -27,8 +27,12 @@
<el-option label="原料" value="raw_material" />
</el-select>
</el-form-item> -->
<el-form-item label="卷号">
<el-input v-model="queryParams.currentCoilNo" placeholder="请输入卷号" clearable size="small"
<el-form-item label="入场卷号">
<el-input v-model="queryParams.enterCoilNo" placeholder="请输入入场卷号" clearable size="small"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="当前卷号">
<el-input v-model="queryParams.currentCoilNo" placeholder="请输入当前卷号" clearable size="small"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="物料">
@@ -192,7 +196,7 @@ export default {
},
dialogWidth: {
type: String,
default: '1000px'
default: '1200px'
},
// 过滤条件(可以预设一些查询条件)
filters: {

View File

@@ -15,25 +15,47 @@
<i class="el-icon-document"></i>
<span class="file-name">{{ file.originalName }}</span>
</div>
<el-button
type="text"
icon="el-icon-download"
@click="downloadFile(file)"
size="small"
class="download-btn"
>
下载
</el-button>
<div class="file-actions">
<el-button
type="text"
icon="el-icon-view"
@click="handlePreview(file)"
size="small"
class="preview-btn"
>
预览
</el-button>
<el-button
type="text"
icon="el-icon-download"
@click="downloadFile(file)"
size="small"
class="download-btn"
>
下载
</el-button>
</div>
</div>
</div>
<!-- 文件预览组件 -->
<file-preview
:visible.sync="previewVisible"
:file-url="previewFileUrl"
:file-name="previewFileName"
/>
</div>
</template>
<script>
import { listByIds } from "@/api/system/oss";
import FilePreview from "../FilePreview";
export default {
name: "FileList",
components: {
FilePreview
},
props: {
ossIds: {
type: String,
@@ -43,7 +65,11 @@ export default {
data() {
return {
fileList: [],
loading: false // 加载状态
loading: false, // 加载状态
// 预览相关
previewVisible: false,
previewFileUrl: '',
previewFileName: ''
}
},
watch: {
@@ -81,6 +107,12 @@ export default {
return;
}
this.$download.oss(file.ossId);
},
// 预览文件
handlePreview(file) {
this.previewFileUrl = file.url;
this.previewFileName = file.originalName;
this.previewVisible = true;
}
}
}
@@ -115,6 +147,12 @@ export default {
transition: background-color 0.3s;
}
.file-actions {
display: flex;
align-items: center;
gap: 16px;
}
.file-item:last-child {
border-bottom: none;
}

View File

@@ -0,0 +1,185 @@
<template>
<el-dialog
:title="title"
:visible.sync="dialogVisible"
:width="width"
:close-on-click-modal="false"
destroy-on-close
append-to-body
@close="handleClose"
>
<!-- 图片预览 -->
<div v-if="fileType === 'image'" class="preview-image">
<div class="image-controls">
<el-button type="primary" size="small" @click="zoomIn">放大</el-button>
<el-button type="primary" size="small" @click="zoomOut">缩小</el-button>
<el-button type="primary" size="small" @click="resetZoom">重置</el-button>
</div>
<div class="image-container" ref="imageContainer">
<img
:src="fileUrl"
:style="{ transform: `scale(${scale})` }"
class="preview-image-content"
@wheel="handleWheel"
/>
</div>
</div>
<!-- PDF预览 -->
<div v-else-if="fileType === 'pdf'" class="preview-pdf">
<iframe
:src="fileUrl"
class="preview-pdf-content"
frameborder="0"
/>
</div>
<!-- 不支持的文件类型 -->
<div v-else class="preview-not-supported">
<el-empty description="暂不支持预览此文件类型"></el-empty>
</div>
</el-dialog>
</template>
<script>
export default {
name: "FilePreview",
props: {
visible: {
type: Boolean,
default: false
},
fileUrl: {
type: String,
required: true
},
fileName: {
type: String,
default: "文件预览"
},
width: {
type: String,
default: "80%"
}
},
data() {
return {
dialogVisible: false,
scale: 1
};
},
watch: {
visible: {
handler(val) {
this.dialogVisible = val;
},
immediate: true
},
dialogVisible(val) {
if (!val) {
this.$emit('update:visible', false);
}
}
},
computed: {
title() {
return this.fileName || "文件预览";
},
fileType() {
const fileName = this.fileName || '';
const ext = fileName.split('.').pop()?.toLowerCase();
if (['png', 'jpg', 'jpeg', 'bmp', 'webp'].includes(ext)) {
return 'image';
} else if (ext === 'pdf') {
return 'pdf';
} else {
return 'other';
}
}
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
// 放大图片
zoomIn() {
if (this.scale < 3) {
this.scale += 0.1;
}
},
// 缩小图片
zoomOut() {
if (this.scale > 0.1) {
this.scale -= 0.1;
}
},
// 重置缩放
resetZoom() {
this.scale = 1;
},
// 鼠标滚轮缩放
handleWheel(event) {
event.preventDefault();
const delta = event.deltaY > 0 ? -0.1 : 0.1;
if ((this.scale > 0.1 || delta > 0) && (this.scale < 3 || delta < 0)) {
this.scale += delta;
}
}
}
};
</script>
<style scoped>
.preview-image {
width: 100%;
height: 70vh;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
}
.image-controls {
padding: 10px;
display: flex;
gap: 10px;
border-bottom: 1px solid #e4e7ed;
background-color: #ffffff;
}
.image-container {
flex: 1;
overflow: auto;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
}
.preview-image-content {
transition: transform 0.3s ease;
cursor: zoom-in;
max-width: 100%;
max-height: 100%;
}
.preview-image-content:hover {
cursor: zoom-in;
}
.preview-pdf {
width: 100%;
height: 70vh;
}
.preview-pdf-content {
width: 100%;
height: 100%;
}
.preview-not-supported {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
</style>

View File

@@ -32,19 +32,31 @@
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handlePreview(file)" type="primary">预览</el-link>
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
</div>
</li>
</transition-group>
<!-- 文件预览组件 -->
<file-preview
:visible.sync="previewVisible"
:file-url="previewFileUrl"
:file-name="previewFileName"
/>
</div>
</template>
<script>
import { getToken } from "@/utils/auth";
import { listByIds, delOss } from "@/api/system/oss";
import FilePreview from "../FilePreview";
export default {
name: "FileUpload",
components: {
FilePreview
},
props: {
// 值
value: [String, Object, Array],
@@ -85,6 +97,10 @@ export default {
},
fileList: [],
loading: false,
// 预览相关
previewVisible: false,
previewFileUrl: '',
previewFileName: ''
};
},
watch: {
@@ -232,6 +248,12 @@ export default {
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
},
// 预览文件
handlePreview(file) {
this.previewFileUrl = file.url;
this.previewFileName = file.name;
this.previewVisible = true;
},
},
};
</script>

View File

@@ -1,6 +1,6 @@
<template>
<el-select filterable v-model="_customerId" remote :remote-method="remoteSearchCustomer" :loading="customerLoading" placeholder="请选择客户">
<el-option v-for="item in customerList" :key="item.customerId" :label="item.name" :value="item.customerId" />
<el-select filterable v-model="_customerId" remote :remote-method="remoteSearchCustomer" :style="style" :loading="customerLoading" placeholder="请选择客户">
<el-option v-for="item in customerList" :key="item[bindField]" :label="item.companyName" :value="item[bindField]" />
</el-select>
</template>
@@ -13,6 +13,14 @@
value: {
type: String,
default: ''
},
bindField: {
type: String,
default: 'customerId'
},
style: {
type: Object,
default: () => ({})
}
},
computed: {
@@ -22,6 +30,12 @@
},
set(value) {
this.$emit('input', value);
// 找到对应的客户信息
const customer = this.customerList.find(item => item[this.bindField] === value);
// 触发 change 事件,传递客户信息
if (customer) {
this.$emit('change', customer);
}
}
}
},
@@ -37,7 +51,7 @@
methods: {
remoteSearchCustomer(query) {
this.customerLoading = true;
listCustomer({ name: query, pageNum: 1, pageSize: 10 }).then(response => {
listCustomer({ companyName: query, pageNum: 1, pageSize: 10 }).then(response => {
this.customerList = response.rows;
}).finally(() => {
this.customerLoading = false;