feat: 退款管理

This commit is contained in:
砂糖
2025-09-18 10:20:37 +08:00
parent 562f12df01
commit 8d9d7bac33
6 changed files with 404 additions and 441 deletions

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询订单下人员工作及扣款信息列表
export function listWorkDeduction(query) {
return request({
url: '/oa/workDeduction/list',
method: 'get',
params: query
})
}
// 查询订单下人员工作及扣款信息详细
export function getWorkDeduction(deductionId) {
return request({
url: '/oa/workDeduction/' + deductionId,
method: 'get'
})
}
// 新增订单下人员工作及扣款信息
export function addWorkDeduction(data) {
return request({
url: '/oa/workDeduction',
method: 'post',
data: data
})
}
// 修改订单下人员工作及扣款信息
export function updateWorkDeduction(data) {
return request({
url: '/oa/workDeduction',
method: 'put',
data: data
})
}
// 删除订单下人员工作及扣款信息
export function delWorkDeduction(deductionId) {
return request({
url: '/oa/workDeduction/' + deductionId,
method: 'delete'
})
}

View File

@@ -5,17 +5,14 @@
<!-- 左侧使用klp-list组件占6列 -->
<el-col :span="6" style="display: table-cell;">
<!-- 搜索表单 - 精简搜索项 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px"
class="mb-4">
<el-row :gutter="10">
<el-col :span="16">
<el-input v-model="queryParams.orderCode" placeholder="请输入订单编号" clearable @change="handleQuery" />
</el-col>
<el-col :span="8">
<el-button type="primary" plain icon="Plus" size="small" @click="handleAdd" class="w-full">新增订单</el-button>
</el-col>
</el-row>
</el-form>
<el-row :gutter="10">
<el-col :span="16">
<el-input v-model="queryParams.orderCode" placeholder="请输入订单编号" clearable @change="handleQuery" />
</el-col>
<el-col :span="8">
<el-button type="primary" plain icon="Plus" size="small" @click="handleAdd" class="w-full">新增订单</el-button>
</el-col>
</el-row>
<!-- klp-list组件 -->
<klp-list :list-data="orderList" :model-value="selectedIds" title-field="orderCode" list-key="orderId"
@@ -105,7 +102,7 @@
</el-tab-pane>
<el-tab-pane label="退换货" name="returnExchange">
<div class="mt-4">
<div class="mt-4">
<ReturnExchange :orderId="selectedOrderId" />
</div>
</el-tab-pane>

View File

@@ -67,8 +67,12 @@
<!-- 添加或修改退换货管理对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="returnExchangeRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="关联订单明细ID" prop="orderDetailId">
<el-input v-model="form.orderDetailId" placeholder="请输入关联订单明细ID" />
<el-form-item label="订单明细" prop="orderDetailId">
<el-select v-model="form.orderDetailId" placeholder="请选择订单明细">
<el-option v-for="item in orderDetailList" :key="item.detailId" :value="item.detailId">
<ProductInfo :productId="item.productId" />
</el-option>
</el-select>
</el-form-item>
<el-form-item label="客户ID" prop="customerId">
<CustomerSelect v-model="form.customerId" />
@@ -95,7 +99,9 @@
<script setup name="ReturnExchange">
import { listReturnExchange, getReturnExchange, delReturnExchange, addReturnExchange, updateReturnExchange } from "@/api/oa/returnExchange";
import { listOrderDetail } from "@/api/oms/orderDetail";
import CustomerSelect from '@/components/CustomerSelect/index.vue';
import ProductInfo from '@/components/Renderer/ProductInfo.vue';
const { proxy } = getCurrentInstance();
@@ -140,6 +146,11 @@ const data = reactive({
const { queryParams, form, rules } = toRefs(data);
watch(() => props.orderId, () => {
getOrderDetailList();
getList();
}, { immediate: true });
/** 查询退换货管理列表 */
function getList() {
loading.value = true;
@@ -150,6 +161,15 @@ function getList() {
});
}
const orderDetailList = ref([]);
function getOrderDetailList() {
listOrderDetail({
orderId: props.orderId,
}).then(response => {
orderDetailList.value = response.rows;
});
}
// 取消按钮
function cancel() {
open.value = false;

View File

@@ -0,0 +1,328 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="订单编号" prop="orderId">
<el-select style="width: 150px;" clearable v-model="queryParams.orderId" placeholder="请选择订单编号" filterable :loading="loading">
<el-option v-for="item in orderList" :key="item.orderId" :value="item.orderId" :label="item.orderCode"/>
</el-select>
</el-form-item>
<el-form-item label="人员名字" prop="personName">
<el-input
v-model="queryParams.personName"
placeholder="请输入人员名字"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="扣款原因" prop="deductionReason">
<el-input
v-model="queryParams.deductionReason"
placeholder="请输入扣款原因"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="工作日期" prop="workDate">
<el-date-picker clearable
v-model="queryParams.workDate"
type="date"
value-format="YYYY-MM-DD 00:00:00"
placeholder="请选择工作日期">
</el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="Delete"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="warning"
plain
icon="Download"
@click="handleExport"
>导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="workDeductionList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="订单编号" align="center" prop="orderId" />
<el-table-column label="人员名字" align="center" prop="personName" />
<el-table-column label="工作内容" align="center" prop="workContent" />
<el-table-column label="扣款金额" align="center" prop="deductionAmount" />
<el-table-column label="扣款原因" align="center" prop="deductionReason" />
<el-table-column label="工作日期" align="center" prop="workDate" width="180">
<template #default="scope">
<span>{{ formatterTime(scope.row.workDate) }}</span>
</template>
</el-table-column>
<!-- <el-table-column label="状态0-有效1-无效" align="center" prop="status" /> -->
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改订单下人员工作及扣款信息对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="workDeductionRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="关联订单" prop="orderId">
<el-select style="width: 150px;" v-model="form.orderId" placeholder="请选择关联订单" filterable remote :loading="loading" @remote-change="remoteOrderList">
<!-- 支持远程搜索 -->
<el-option v-for="item in orderList" :key="item.orderId" :value="item.orderId" :label="item.orderCode" />
</el-select>
</el-form-item>
<el-form-item label="人员名字" prop="personName">
<el-input v-model="form.personName" placeholder="请输入人员名字" />
</el-form-item>
<el-form-item label="工作内容">
<el-input v-model="form.workContent" placeholder="请输入工作内容" />
</el-form-item>
<el-form-item label="扣款金额" prop="deductionAmount">
<el-input v-model="form.deductionAmount" placeholder="请输入扣款金额" />
</el-form-item>
<el-form-item label="扣款原因" prop="deductionReason">
<el-input v-model="form.deductionReason" placeholder="请输入扣款原因" />
</el-form-item>
<el-form-item label="工作日期" prop="workDate">
<el-date-picker clearable
v-model="form.workDate"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="请选择工作日期">
</el-date-picker>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="WorkDeduction">
import { listWorkDeduction, getWorkDeduction, delWorkDeduction, addWorkDeduction, updateWorkDeduction } from "@/api/oms/workDeduction";
import { listOrder } from "@/api/oms/order";
const { proxy } = getCurrentInstance();
const workDeductionList = ref([]);
const open = ref(false);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const title = ref("");
const formatterTime = (time) => {
return proxy.parseTime(time, '{y}-{m}-{d}')
}
const data = reactive({
form: {},
queryParams: {
pageNum: 1,
pageSize: 10,
orderId: undefined,
personName: undefined,
workContent: undefined,
deductionAmount: undefined,
deductionReason: undefined,
workDate: undefined,
status: undefined,
},
rules: {
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询订单下人员工作及扣款信息列表 */
function getList() {
loading.value = true;
listWorkDeduction(queryParams.value).then(response => {
workDeductionList.value = response.rows;
total.value = response.total;
loading.value = false;
});
}
const orderList = ref([]);
function remoteOrderList(value) {
listOrder({
orderCode: value,
pageNum: 1,
pageSize: 1000,
}).then(response => {
orderList.value = response.rows;
});
}
// 取消按钮
function cancel() {
open.value = false;
reset();
}
// 表单重置
function reset() {
form.value = {
deductionId: null,
orderId: null,
personName: null,
workContent: null,
deductionAmount: null,
deductionReason: null,
workDate: null,
status: null,
remark: null,
createTime: null,
createBy: null,
updateTime: null,
updateBy: null,
delFlag: null
};
proxy.resetForm("workDeductionRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
// 多选框选中数据
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.deductionId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
function handleAdd() {
reset();
open.value = true;
title.value = "添加订单下人员工作及扣款信息";
}
/** 修改按钮操作 */
function handleUpdate(row) {
loading.value = true
reset();
const _deductionId = row.deductionId || ids.value
getWorkDeduction(_deductionId).then(response => {
loading.value = false;
form.value = response.data;
open.value = true;
title.value = "修改订单下人员工作及扣款信息";
});
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["workDeductionRef"].validate(valid => {
if (valid) {
buttonLoading.value = true;
if (form.value.deductionId != null) {
updateWorkDeduction(form.value).then(response => {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
}).finally(() => {
buttonLoading.value = false;
});
} else {
addWorkDeduction(form.value).then(response => {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
}).finally(() => {
buttonLoading.value = false;
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
const _deductionIds = row.deductionId || ids.value;
proxy.$modal.confirm('是否确认删除订单下人员工作及扣款信息编号为"' + _deductionIds + '"的数据项?').then(function() {
loading.value = true;
return delWorkDeduction(_deductionIds);
}).then(() => {
loading.value = true;
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
loading.value = false;
});
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('oa/workDeduction/export', {
...queryParams.value
}, `workDeduction_${new Date().getTime()}.xlsx`)
}
getList();
remoteOrderList()
</script>

View File

@@ -226,14 +226,12 @@
<script>
import { listStockIo, getStockIo, delStockIo, addStockIo, updateStockIo } from "@/api/wms/stockIo";
import { listStockIoDetail } from "@/api/wms/stockIoDetail";
import StockIoDetailPanel from './panels/detail.vue';
import BarcodeGenerator from './panels/barcode.vue';
export default {
name: "StockIo",
dicts: ['stock_biz_type', 'stock_io_type', 'stock_status'],
components: { StockIoDetailPanel, BarcodeGenerator },
components: { StockIoDetailPanel },
data() {
return {
// 按钮loading

View File

@@ -1,424 +0,0 @@
<template>
<div class="barcode-3col-layout">
<!-- 预览区 -->
<div class="barcode-preview-col">
<div class="iframe-wrapper">
<iframe ref="previewIframe" class="barcode-iframe" frameborder="0" :style="iframeStyle"></iframe>
</div>
</div>
<!-- 右侧控制+设置区 -->
<div class="barcode-right-col">
<!-- 控制区 -->
<div class="barcode-control-bar">
<el-tabs v-model="activeTab" stretch>
<el-tab-pane label="排版设置" name="layout" />
<el-tab-pane label="二维码明细" name="detail" />
</el-tabs>
</div>
<!-- 设置区 -->
<div class="barcode-settings-panel">
<el-form v-if="activeTab==='layout'" label-width="80px" size="small" label-position="top">
<!-- 排版设置内容保持不变 -->
<el-form-item label="每行数量">
<el-input-number :controls=false controls-position="right" v-model="perRow" size="mini" :min="1" :max="10" />
</el-form-item>
<el-form-item label="二维码尺寸">
<el-input-number :controls=false controls-position="right" v-model="barcodeWidth" size="mini" :min="60" :max="600" />
</el-form-item>
<el-form-item label="纸张尺寸">
<el-select v-model="paperSize" placeholder="请选择纸张尺寸" style="width: 160px">
<el-option label="A4 (210mm x 297mm)" value="A4" />
<el-option label="A5 (148mm x 210mm)" value="A5" />
<el-option label="A6 (105mm x 148mm)" value="A6" />
<el-option label="自定义" value="custom" />
</el-select>
</el-form-item>
<el-form-item v-if="paperSize==='custom'" label="自定义宽度(mm)">
<el-input-number :controls=false controls-position="right" v-model="customPaperWidth" size="mini" :min="50" :max="500" />
</el-form-item>
<el-form-item v-if="paperSize==='custom'" label="自定义高度(mm)">
<el-input-number :controls=false controls-position="right" v-model="customPaperHeight" size="mini" :min="50" :max="500" />
</el-form-item>
<el-form-item label="方向">
<el-radio-group v-model="paperOrientation">
<el-radio label="portrait">纵向</el-radio>
<el-radio label="landscape">横向</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlePrint">打印</el-button>
</el-form-item>
</el-form>
<el-form v-else label-width="80px" size="small" label-position="top">
<el-form-item label="二维码明细">
<div v-for="(cfg, idx) in barcodeConfigs" :key="idx" style="margin-bottom: 16px; border: 1px solid #eee; border-radius: 4px; padding: 12px 16px; background: #fafbfc;">
<div style="margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: bold; color: #666;">条码 {{ idx + 1 }}</span>
<el-button
type="text"
size="mini"
@click="saveAsImage(cfg.code, cfg.textTpl || cfg.code, idx)"
icon="Download"
>
另存为图片
</el-button>
<el-button
type="text"
size="mini"
@click="handleDelete(cfg, idx)"
icon="Delete"
>
删除
</el-button>
</div>
<el-form-item label="二维码内容" label-width="70px" style="margin-bottom: 8px;">
<el-input disabled type="textarea" v-model="cfg.code" size="mini" :autosize="{ minRows: 1, maxRows: 3 }" placeholder="请输入条码内容" />
</el-form-item>
<el-form-item label="生成数量" label-width="70px" style="margin-bottom: 8px;">
<el-input-number :controls=false controls-position="right" v-model.number="cfg.count" :min="1" :max="100" size="mini" />
</el-form-item>
<el-form-item label="下方文字" label-width="70px" style="margin-bottom: 0;">
<el-input type="textarea" v-model="cfg.textTpl" size="mini" placeholder="如 箱号" />
</el-form-item>
</div>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script>
import QRCode from 'qrcode';
export default {
name: 'BarcodeGenerator',
props: {
barcodes: {
type: Array,
required: true
}
},
data() {
return {
perRow: 3,
barcodeWidth: 180,
barcodeHeight: 180, // 二维码建议宽高一致
barcodeConfigs: [], // [{code, count, textTpl}]
previewScale: 1, // 预览缩放比例
activeTab: 'layout',
paperSize: 'A4',
paperOrientation: 'portrait',
customPaperWidth: 210,
customPaperHeight: 297
};
},
computed: {
expandedBarcodes() {
let arr = [];
this.barcodeConfigs.forEach(cfg => {
for (let i = 0; i < (cfg.count || 1); i++) {
arr.push(cfg.code);
}
});
return arr;
},
expandedBarcodeTexts() {
let arr = [];
this.barcodeConfigs.forEach(cfg => {
for (let i = 0; i < (cfg.count || 1); i++) {
// 模板替换 {{n}}
let text = cfg.textTpl || cfg.code;
text = text.replace(/\{\{n\}\}/g, i + 1);
arr.push(text);
}
});
return arr;
},
expandedBarcodeTableData() {
return this.expandedBarcodes.map((code, idx) => ({ code, text: this.expandedBarcodeTexts[idx] }));
},
barcodeRows() {
const rows = [];
const arr = this.expandedBarcodes;
for (let i = 0; i < arr.length; i += this.perRow) {
rows.push(arr.slice(i, i + this.perRow));
}
return rows;
},
barcodeTextRows() {
const rows = [];
const arr = this.expandedBarcodeTexts;
for (let i = 0; i < arr.length; i += this.perRow) {
rows.push(arr.slice(i, i + this.perRow));
}
return rows;
},
iframeStyle() {
const { width, height } = this.getPaperPx();
return {
width: width + 'px',
minHeight: height + 'px',
height: height + 'px',
background: '#fff',
border: 'none',
display: 'block',
margin: 0,
padding: 0
};
}
},
watch: {
barcodes: {
handler(newVal) {
// 初始化barcodeConfigs
this.barcodeConfigs = newVal;
this.$nextTick(this.renderPreviewIframe);
},
immediate: true
},
barcodeConfigs: {
handler() { this.$nextTick(this.renderPreviewIframe); },
deep: true
},
perRow() { this.$nextTick(this.renderPreviewIframe); },
barcodeWidth() { this.$nextTick(this.renderPreviewIframe); },
barcodeHeight() { this.$nextTick(this.renderPreviewIframe); },
previewScale() { this.$nextTick(this.renderPreviewIframe); },
paperSize() { this.$nextTick(this.renderPreviewIframe); },
paperOrientation() { this.$nextTick(this.renderPreviewIframe); },
customPaperWidth() { this.$nextTick(this.renderPreviewIframe); },
customPaperHeight() { this.$nextTick(this.renderPreviewIframe); }
},
methods: {
getBarcodeId(row, col) {
return `barcode-canvas-${row}-${col}`;
},
getBarcodeText(row, col, code) {
if (
this.barcodeTextRows[row] &&
typeof this.barcodeTextRows[row][col] !== 'undefined' &&
this.barcodeTextRows[row][col] !== null &&
this.barcodeTextRows[row][col] !== ''
) {
return this.barcodeTextRows[row][col];
}
return code;
},
handleDelete(cfg, idx) {
// this.barcodeConfigs.splice(idx, 1);
this.$emit('delete', cfg, idx);
},
getPaperPx() {
// mm to px, 1mm ≈ 3.78px
let width, height;
if (this.paperSize === 'A4') {
width = 210 * 3.78;
height = 297 * 3.78;
} else if (this.paperSize === 'A5') {
width = 148 * 3.78;
height = 210 * 3.78;
} else if (this.paperSize === 'A6') {
width = 105 * 3.78;
height = 148 * 3.78;
} else {
width = this.customPaperWidth * 3.78;
height = this.customPaperHeight * 3.78;
}
if (this.paperOrientation === 'landscape') {
return { width: height, height: width };
}
return { width, height };
},
getPrintHtml() {
const { width, height } = this.getPaperPx();
let html = `
<html>
<head>
<title>打印二维码</title>
<style>
body { margin: 0; padding: 0; background: #fff; }
.barcode-list { width: ${width}px; min-height: ${height}px; margin: 0 auto; background: #fff; }
.barcode-row { display: flex; margin-bottom: 24px; }
.barcode-item { flex: 1; text-align: center; }
.barcode-text { margin-top: 8px; font-size: 14px; }
html, body { overflow-x: hidden; }
</style>
</head>
<body>
<div class="barcode-list">
`;
this.barcodeRows.forEach((row, rowIndex) => {
html += '<div class="barcode-row">';
row.forEach((code, colIndex) => {
const id = this.getBarcodeId(rowIndex, colIndex);
const text = this.getBarcodeText(rowIndex, colIndex, code);
html += `<div class="barcode-item">
<canvas id="${id}" width="${this.barcodeWidth}" height="${this.barcodeHeight}"></canvas>
<div class="barcode-text">${text}</div>
</div>`;
});
html += '</div>';
});
html += '</div></body></html>';
return html;
},
async renderPreviewIframe() {
const iframe = this.$refs.previewIframe;
if (!iframe) return;
const doc = iframe.contentDocument || iframe.contentWindow.document;
doc.open();
doc.write(this.getPrintHtml());
doc.close();
// 渲染二维码
setTimeout(async () => {
for (let rowIndex = 0; rowIndex < this.barcodeRows.length; rowIndex++) {
const row = this.barcodeRows[rowIndex];
for (let colIndex = 0; colIndex < row.length; colIndex++) {
const code = row[colIndex];
const id = this.getBarcodeId(rowIndex, colIndex);
const el = doc.getElementById(id);
if (el) {
await QRCode.toCanvas(el, code, {
width: this.barcodeWidth,
height: this.barcodeHeight,
margin: 0
});
}
}
}
}, 50);
},
handlePrint() {
const iframe = this.$refs.previewIframe;
if (!iframe) return;
iframe.contentWindow.focus();
iframe.contentWindow.print();
},
/**
* 保存二维码为图片
* @param {string} code 二维码内容
* @param {string} text 下方文字
* @param {number} index 索引,用于生成文件名
*/
async saveAsImage(code, text, index) {
try {
// 创建临时canvas用于绘制二维码和文字
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 计算总高度(二维码高度 + 文字区域高度)
const textHeight = 30; // 文字区域高度
canvas.width = this.barcodeWidth;
canvas.height = this.barcodeHeight + textHeight;
// 绘制二维码
const qrCanvas = document.createElement('canvas');
qrCanvas.width = this.barcodeWidth;
qrCanvas.height = this.barcodeHeight;
await QRCode.toCanvas(qrCanvas, code, {
width: this.barcodeWidth,
height: this.barcodeHeight,
margin: 0
});
// 将二维码绘制到主canvas
ctx.drawImage(qrCanvas, 0, 0);
// 绘制文字
ctx.fillStyle = '#000';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(text, this.barcodeWidth / 2, this.barcodeHeight + 5);
// 创建图片链接并下载
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
// 生成文件名(使用索引和内容摘要)
const fileName = `二维码_${index + 1}_${code.substring(0, 10)}.png`;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
this.$message.success('图片保存成功');
} catch (error) {
console.error('保存图片失败:', error);
this.$message.error('保存图片失败,请重试');
}
}
},
mounted() {
this.barcodeConfigs = this.barcodes?.map(b => ({ code: b, count: 1, textTpl: b })) || [];
this.renderPreviewIframe();
}
};
</script>
<style scoped>
/* 样式保持不变 */
.barcode-3col-layout {
display: flex;
flex-direction: row;
width: 100%;
min-height: 400px;
}
.barcode-preview-col {
flex: 1 1 0;
min-width: 0;
background: #fff;
border-right: 1px solid #eee;
padding: 24px 0 24px 24px;
display: flex;
flex-direction: column;
height: 90vh;
overflow: auto;
}
.barcode-right-col {
width: 420px;
min-width: 320px;
padding: 16px;
display: flex;
flex-direction: column;
background: #fafbfc;
height: 90vh;
}
.barcode-control-bar {
border-bottom: 1px solid #eee;
background: #fafbfc;
padding: 0 16px;
}
.barcode-settings-panel {
flex: 1;
overflow: auto;
min-height: 0;
max-height: calc(90vh - 48px); /* 48px 约为tabs高度可根据实际调整 */
}
.preview-toolbar {
margin-bottom: 12px;
display: flex;
align-items: center;
}
.iframe-wrapper {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
background: #f8f8f8;
overflow: auto;
}
.barcode-iframe {
/* width、height 由:style绑定动态控制 */
min-width: 0;
min-height: 0;
border: none;
background: #fff;
border-radius: 4px;
display: block;
}
</style>