Files
klp-oa/klp-ui/src/views/wms/print/index.vue
2025-08-23 13:56:54 +08:00

730 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="print-page-container">
<!-- 页面标题栏 - 增加面包屑导航提升页面定位感 -->
<div class="page-header">
<div class="header-actions">
<el-tooltip content="清空所有已添加的二维码配置" placement="top" :disabled="drawerBarcodeData.length === 0">
<el-button type="default" icon="el-icon-refresh" @click="handleResetAll"
:disabled="drawerBarcodeData.length === 0" class="btn-reset">
清空所有
</el-button>
</el-tooltip>
<!-- <el-tooltip content="仅对完整配置的二维码进行打印预览" placement="top"
:disabled="drawerBarcodeData.length === 0 || !isAllValid">
<el-button type="success" icon="el-icon-printer" @click="handlePrintPreview"
:disabled="drawerBarcodeData.length === 0 || !isAllValid" class="btn-print">
打印预览
</el-button>
</el-tooltip> -->
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">
添加二维码
</el-button>
</div>
</div>
<!-- 主内容区 - 增加卡片外层容器提升整体层次感 -->
<el-row :gutter="24" class="main-content">
<!-- 左侧配置区 - 增加操作提示 -->
<el-col :span="8" class="left-col">
<div class="card-container">
<div class="left-container card">
<!-- 空状态提示 - 增加插图优化文案 -->
<div v-if="drawerBarcodeData.length === 0" class="empty-state">
<div class="empty-icon">
<i class="el-icon-qrcode"></i>
</div>
<div class="empty-text">暂无二维码配置</div>
<div class="empty-desc">点击"添加二维码"按钮开始创建打印配置</div>
<el-button type="primary" size="small" @click="handleAdd" class="empty-action-btn">
立即添加
</el-button>
</div>
<!-- 二维码配置表单列表 - 优化卡片间距和hover效果 -->
<el-form label-width="80px" size="small" label-position="top" v-else class="config-form-list">
<div v-for="(cfg, idx) in drawerBarcodeData" :key="idx" class="config-card"
:class="{ 'invalid-card': !isConfigValid(cfg) && cfg.ioType }" @mouseenter="cfg.hovered = true"
@mouseleave="cfg.hovered = false">
<!-- 配置卡片头部 - 增加序号背景色优化操作按钮显示逻辑 -->
<div class="config-card-header">
<div class="card-title-wrap">
<span class="card-index">{{ idx + 1 }}</span>
<span class="card-title">二维码配置</span>
<el-tag size="mini" type="success" v-if="isConfigValid(cfg)" class="card-status-tag">
已完善
</el-tag>
<el-tag size="mini" type="warning" v-else-if="cfg.ioType" class="card-status-tag">
待完善
</el-tag>
</div>
<div class="card-actions">
<el-tooltip content="另存为图片" placement="top">
<el-button type="text" size="mini" @click="saveAsImage(idx)" icon="el-icon-download"
:disabled="!isConfigValid(cfg)" class="action-btn"></el-button>
</el-tooltip>
<el-tooltip content="删除配置" placement="top">
<el-button type="text" size="mini" @click="handleDelete(cfg, idx)" icon="el-icon-delete"
class="action-btn delete-btn"></el-button>
</el-tooltip>
</div>
</div>
<!-- 配置表单内容 - 优化间距增加字段分组 -->
<div class="config-form-content">
<el-row :gutter="8">
<el-col :span="12">
<el-form-item label="操作类型" :error="getError(cfg, 'ioType')" class="form-item">
<el-select
v-model="cfg.ioType"
placeholder="请选择操作类型"
class="form-select"
@change="(value) => {
const prefix = value == 'in' ? '入库' : value == 'out' ? '出库' : value == 'transfer' ? '移库' : ''
cfg.text = prefix + '二维码' + new Date().getTime()
}"
>
<el-option label="入库" value="in" />
<el-option label="出库" value="out" />
<el-option label="移库" value="transfer" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
:label="cfg.ioType == 'in' ? '入库单据号' : cfg.ioType == 'out' ? '出库单据号' : cfg.ioType == 'transfer' ? '移库单据号' : '单据号'"
:error="getError(cfg, 'stockIoId')"
class="form-item"
>
<el-select clearable filterable size="mini" v-model="cfg.stockIoId" placeholder="请选择挂载单据"
class="form-select" :disabled="!cfg.ioType">
<el-option v-for="item in masterList.filter(i => i.ioType === cfg.ioType)"
:key="item.stockIoId" :label="item.stockIoCode" :value="item.stockIoId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="8">
<el-col :span="12">
<el-form-item label="目标仓库" :error="getError(cfg, 'warehouseId')" class="form-item">
<WarehouseSelect v-model="cfg.warehouseId" :disabled="!cfg.ioType" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="cfg.ioType === 'transfer'">
<el-form-item label="源仓库" :error="getError(cfg, 'fromWarehouseId')" class="form-item">
<WarehouseSelect v-model="cfg.fromWarehouseId" :disabled="!cfg.ioType" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="8">
<el-col :span="12">
<el-form-item label="物料类型" :error="getError(cfg, 'itemType')" class="form-item">
<el-select v-model="cfg.itemType" placeholder="请选择物料类型" class="form-select">
<el-option v-for="dict in dict.type.stock_item_type" :key="dict.value" :label="dict.label"
:value="dict.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="物料信息" :error="getError(cfg, 'itemId')" class="form-item">
<ProductSelect v-if="cfg.itemType === 'product'" v-model="cfg.itemId" placeholder="请选择产品"
@change="onItemChange($event, idx)" :disabled="!cfg.itemType" />
<SemiSelect v-else-if="cfg.itemType === 'semi'" v-model="cfg.itemId" placeholder="请选择半成品"
@change="onItemChange($event, idx)" :disabled="!cfg.itemType" />
<RawMaterialSelect v-else-if="cfg.itemType === 'raw_material'" v-model="cfg.itemId"
placeholder="请选择原材料" @change="onItemChange($event, idx)" :disabled="!cfg.itemType" />
<el-input v-else disabled v-model="cfg.itemId" placeholder="请先选择物料类型" style="width: 100%;" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="8">
<el-col :span="12">
<el-form-item label="物料数量" :error="getError(cfg, 'count')" class="form-item">
<el-input-number v-model="cfg.count" :min="1" :max="100" size="mini" placeholder="请输入数量" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="下方文字" class="form-item">
<el-input v-model="cfg.text" size="mini" placeholder="例如:产品入库二维码" />
</el-form-item>
</el-col>
</el-row>
<!-- 配置状态提示 - 优化背景色和图标 -->
<div v-if="!isConfigValid(cfg) && cfg.ioType" class="config-warning">
<i class="el-icon-warning-outline"></i>
<span>请完善必填字段配置标红项为未完成</span>
</div>
</div>
</div>
</el-form>
</div>
</div>
</el-col>
<!-- 右侧预览区 - 增加预览说明优化加载状态 -->
<el-col :span="16" class="right-col">
<div class="card-container">
<div class="right-container card">
<div class="right-header">
<div class="preview-count">
{{ drawerBarcodeData.length }} 个二维码
<span v-if="drawerBarcodeData.length > 0">| 有效配置: {{ validConfigCount }} </span>
</div>
</div>
<!-- 预览区内容 - 增加预览容器边框优化滚动体验 -->
<div class="preview-content">
<div v-loading="loading || !drawerBarcodeData.length" class="preview-loading"
:element-loading-text="drawerBarcodeData.length === 0 ? '暂无配置数据无法预览' : '加载预览中...'"
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.9)"
element-loading-offset="100">
<BarCode :barcodes="barCodeConfigs" v-if="drawerBarcodeData.length > 0" class="barcode-renderer" />
</div>
<!-- 预览为空提示 - 优化空状态样式 -->
<div v-if="drawerBarcodeData.length > 0 && validConfigCount === 0" class="preview-empty">
<div class="empty-icon">
<i class="el-icon-info"></i>
</div>
<div class="empty-text">所有二维码配置均不完整</div>
<div class="empty-desc">请完善左侧标红的必填字段后再查看预览</div>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
// 保持原脚本逻辑不变仅增加hover状态初始化
import BarCode from './components/CodeRenderer.vue';
import ProductSelect from '@/components/KLPService/ProductSelect/index.vue';
import SemiSelect from '@/components/KLPService/SemiSelect/index.vue';
import RawMaterialSelect from '@/components/KLPService/RawMaterialSelect/index.vue';
import WarehouseSelect from '@/components/WarehouseSelect/index.vue';
import { saveAsImage } from '@/utils/klp';
import { listStockIo } from '@/api/wms/stockIo';
export default {
name: 'Print',
components: {
BarCode,
ProductSelect,
SemiSelect,
RawMaterialSelect,
WarehouseSelect
},
dicts: ['stock_item_type', 'stock_io_type'],
data() {
return {
drawerBarcodeData: [], // 条码数据
loading: false, // 加载状态
masterList: [], // 单据列表
};
},
mounted() {
this.fetchMaster();
},
computed: {
// 有效的二维码配置数量
validConfigCount() {
return this.drawerBarcodeData.filter(cfg => this.isConfigValid(cfg)).length;
},
// 所有配置是否都有效
isAllValid() {
return this.drawerBarcodeData.length > 0 &&
this.drawerBarcodeData.every(cfg => this.isConfigValid(cfg));
},
// 二维码配置数据
barCodeConfigs() {
return this.drawerBarcodeData
.filter(cfg => this.isConfigValid(cfg))
.map(b => ({
code: JSON.stringify({
ioType: b.ioType,
stockIoId: b.stockIoId,
fromWarehouseId: b.fromWarehouseId,
warehouseId: b.warehouseId,
itemType: b.itemType,
itemId: b.itemId,
batchNo: b.batchNo || 'auto',
quantity: b.count || 1,
unit: b.unit || '',
recordType: 1,
}),
count: 1,
textTpl: b.text || '二维码'
}));
}
},
methods: {
// 物料选择变更
onItemChange(item, idx) {
if (item && this.drawerBarcodeData[idx]) {
this.drawerBarcodeData[idx].unit = item.unit;
// 如果未设置数量默认设置为1
if (!this.drawerBarcodeData[idx].count) {
this.drawerBarcodeData[idx].count = 1;
}
}
},
// 获取单据列表
fetchMaster() {
this.loading = true;
listStockIo({ pageSize: 9999, pageNum: 1 })
.then(res => {
this.masterList = res.rows || [];
this.$message.success({
message: '单据列表加载成功',
duration: 1500
});
})
.catch(error => {
console.error("获取挂载单据失败", error);
this.$message.error("获取挂载单据失败,请稍后重试");
})
.finally(() => {
this.loading = false;
});
},
// 删除二维码配置
handleDelete(cfg, idx) {
this.$confirm('确定要删除这个二维码配置吗?', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.drawerBarcodeData.splice(idx, 1);
this.$message.success({
message: '删除成功',
duration: 1500
});
}).catch(() => {
// 取消删除
});
},
// 添加二维码配置 - 增加hover状态初始化
handleAdd() {
const newConfig = {
ioType: undefined,
stockIoId: undefined,
fromWarehouseId: undefined,
warehouseId: undefined,
itemType: undefined,
itemId: undefined,
batchNo: 'auto',
count: 1, // 默认数量1
unit: '',
text: '二维码', // 默认文字
hovered: false // 新增hover状态用于交互反馈
};
this.drawerBarcodeData.push(newConfig);
// 滚动到最新添加的配置
this.$nextTick(() => {
const container = document.querySelector('.left-container');
container.scrollTop = container.scrollHeight;
});
},
// 保存为图片
saveAsImage(index) {
const config = this.drawerBarcodeData[index];
if (!this.isConfigValid(config)) {
this.$message.warning('配置不完整,无法保存图片');
return;
}
try {
saveAsImage(
this.barCodeConfigs[index].code,
this.barCodeConfigs[index].textTpl,
index,
this
);
this.$message.success('图片保存成功');
} catch (error) {
console.error('保存图片失败', error);
this.$message.error('保存图片失败,请稍后重试');
}
},
// 清空所有配置
handleResetAll() {
this.$confirm('确定要清空所有二维码配置吗?此操作不可恢复', '确认清空', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.drawerBarcodeData = [];
this.$message.success({
message: '已清空所有配置',
duration: 1500
});
}).catch(() => {
// 取消清空
});
},
// 打印预览 - 增加加载状态提示
handlePrintPreview() {
const printLoading = this.$loading({
lock: true,
text: '正在准备打印预览...',
spinner: 'el-icon-loading',
background: 'rgba(255, 255, 255, 0.8)'
});
// 模拟预览准备时间(实际项目可替换为真实接口请求)
setTimeout(() => {
window.print();
printLoading.close();
}, 1000);
},
// 验证单个配置是否有效
isConfigValid(cfg) {
if (!cfg) return false;
// 基础必填字段验证
const basicValid = !!cfg.ioType && !!cfg.stockIoId && !!cfg.warehouseId;
// 移库需要额外验证源仓库
const transferValid = cfg.ioType !== 'transfer' || !!cfg.fromWarehouseId;
// 物料相关验证
const itemValid = !!cfg.itemType && !!cfg.itemId && !!cfg.count && cfg.count >= 1;
return basicValid && transferValid && itemValid;
},
// 获取字段错误信息
getError(cfg, field) {
if (!cfg.ioType) return '';
switch (field) {
case 'ioType':
return !cfg.ioType ? '请选择操作类型' : '';
case 'stockIoId':
return !cfg.stockIoId ? '请选择挂载单据' : '';
case 'warehouseId':
return !cfg.warehouseId ? '请选择目标仓库' : '';
case 'fromWarehouseId':
return cfg.ioType === 'transfer' && !cfg.fromWarehouseId ? '请选择源仓库' : '';
case 'itemType':
return !cfg.itemType ? '请选择物料类型' : '';
case 'itemId':
return !cfg.itemId ? '请选择物料信息' : '';
case 'count':
return !cfg.count || cfg.count < 1 ? '请输入有效的数量至少1' : '';
default:
return '';
}
}
}
};
</script>
<style scoped lang="scss">
// 全局变量定义 - 便于统一维护
$primary-color: #409eff;
$success-color: #67c23a;
$warning-color: #e6a23c;
$danger-color: #f56c6c;
$gray-light: #f5f7fa;
$gray-mid: #e5e7eb;
$gray-dark: #6b7280;
$text-primary: #1f2329;
$text-secondary: #6b7280;
$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
$card-hover-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
// 页面整体容器 - 优化高度计算、增加内边距
.print-page-container {
padding: 16px 20px; // Slightly reduced padding
background-color: $gray-light;
height: calc(100vh - 100px);
box-sizing: border-box;
overflow: hidden;
}
// 页面标题栏 - 增加面包屑、优化布局
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px; // Reduced space between header and content
padding-bottom: 8px;
border-bottom: 1px solid $gray-mid;
.header-breadcrumb {
.el-breadcrumb {
margin-bottom: 4px;
}
.page-title {
font-size: 18px; // Slightly smaller title
font-weight: 600;
color: $text-primary;
margin: 0;
}
}
.header-actions {
display: flex;
gap: 12px; // Reduced gap between buttons
.btn-reset {
padding: 6px 12px; // Smaller padding
transition: all 0.2s ease;
}
.btn-print {
padding: 6px 12px; // Smaller padding
transition: all 0.2s ease;
}
}
}
// 主内容区 - 优化间距
.main-content {
height: calc(100% - 80px);
display: flex;
gap: 16px; // Reduced gap between left and right content
.left-col,
.right-col {
height: 100%;
display: flex;
flex-direction: column;
}
.card-container {
padding: 1px;
flex: 1;
overflow: hidden;
}
}
// 左侧容器 - 优化滚动体验
.left-container {
height: 100%;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
&:hover {
::-webkit-scrollbar {
width: 6px;
height: 6px;
display: block;
}
::-webkit-scrollbar-thumb {
background: #d1d5db;
display: block;
border-radius: 3px;
transition: background 0.3s ease;
&:hover {
background: #9ca3af;
}
}
::-webkit-scrollbar-track {
background: #f9fafb;
display: block;
border-radius: 3px;
}
}
}
// 配置卡片 - 优化阴影和间距
.config-card {
border: 1px solid $gray-mid;
border-radius: 8px;
transition: all 0.3s ease;
background-color: #fff;
overflow: hidden;
&:hover {
border-color: $primary-color;
box-shadow: $card-hover-shadow;
}
&.invalid-card {
border-color: rgba($danger-color, 0.3);
background-color: rgba($danger-color, 0.02);
&:hover {
border-color: $danger-color;
}
}
}
// 配置卡片头部 - 优化布局和样式
.config-card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px; // Reduced padding
border-bottom: 1px solid $gray-mid;
background-color: #f9fafb;
.card-title-wrap {
display: flex;
align-items: center;
gap: 6px; // Reduced gap between elements
.card-index {
width: 18px; // Smaller index
height: 18px; // Smaller index
font-size: 10px; // Smaller font size for index
}
.card-title {
font-size: 12px; // Smaller title
}
.card-status-tag {
margin-left: 6px; // Reduced margin
}
}
.card-actions {
display: flex;
gap: 6px; // Reduced gap between actions
.action-btn {
color: $text-secondary;
transition: color 0.2s ease;
font-size: 12px; // Reduced font size for actions
&:hover {
color: $primary-color;
}
&.delete-btn {
color: $danger-color;
&:hover {
color: #e53e3e;
}
}
&:disabled {
color: $gray-dark;
opacity: 0.5;
cursor: not-allowed;
}
}
}
}
// 配置表单内容 - 优化间距和分组
.config-form-content {
padding: 12px; // Reduced padding
.form-group {
margin-bottom: 12px; // Reduced spacing between groups
.group-title {
font-size: 12px; // Smaller font size for group title
margin-bottom: 8px; // Reduced margin
}
}
.form-item {
margin-bottom: 8px !important; // Reduced spacing between form items
}
.form-select {
width: 100% !important;
}
.config-warning {
margin-top: 8px; // Reduced margin
padding: 8px 10px; // Reduced padding
font-size: 12px; // Smaller font size
}
}
// 空状态样式 - 优化间距和图标
.empty-state,
.preview-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0; // Reduced padding
color: $text-secondary;
text-align: center;
.empty-icon {
font-size: 48px; // Slightly smaller icon
color: $gray-mid;
margin-bottom: 16px; // Reduced margin
}
.empty-text {
font-size: 14px; // Slightly smaller text
margin-bottom: 6px; // Reduced margin
}
.empty-desc {
font-size: 12px; // Smaller description text
margin-bottom: 16px;
}
.empty-action-btn {
padding: 4px 12px; // Smaller button padding
font-size: 12px; // Smaller font size
}
}
// 响应式调整 - 优化小屏幕体验
@media (max-width: 1200px) {
.main-content {
flex-direction: column;
gap: 16px;
}
.left-col,
.right-col {
span: 24;
}
.print-page-container {
padding: 12px; // Reduced padding for smaller screens
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 8px; // Reduced gap
}
}
@media print {
.page-header,
.left-col,
.preview-count {
display: none !important;
}
.print-page-container,
.main-content,
.right-col,
.card-container,
.right-container,
.preview-content {
height: auto !important;
padding: 0 !important;
margin: 0 !important;
box-shadow: none !important;
border: none !important;
}
.barcode-renderer {
gap: 20px; // Reduced gap for print
justify-content: center;
}
}
</style>