feat: 坯布出仓增加操作语音并精简扫码明细

This commit is contained in:
2026-10-13 17:13:01 +08:00
parent 322ada8428
commit 7b199ba559
14 changed files with 78 additions and 159 deletions
+7 -61
View File
@@ -61,6 +61,12 @@ export const STATUS_ACTION_CONFIG = {
unapprove: { status: AUDIT_STATUS.PENDING, msg: '确认消审该单据?', apiKey: 'statusWait' }, unapprove: { status: AUDIT_STATUS.PENDING, msg: '确认消审该单据?', apiKey: 'statusWait' },
}; };
/** 审核 / 作废成功后播放的语音文案 */
export const STATUS_ACTION_VOICE = {
approve: '审核成功',
void: '作废成功',
};
// 出仓类型:当前后端仅提供「坯布其他出货单」一种类型的接口 // 出仓类型:当前后端仅提供「坯布其他出货单」一种类型的接口
export const BILL_TYPES = [ export const BILL_TYPES = [
{ value: 'other', label: '坯布其他出货单' }, { value: 'other', label: '坯布其他出货单' },
@@ -147,17 +153,7 @@ export function normalizeOrder(order) {
}; };
} }
/** 扫码详情区表格列 */ /** 扫码详情区汇总维度(勾选后随扫码请求把对应 lock 字段传给后端) */
export const SCAN_DETAIL_TABLE_COLUMNS = [
{ label: '坯布编号', key: 'grey_fabric_code', width: 140 },
{ label: '坯布名称', key: 'grey_fabric_name', width: 140 },
{ label: '纱批', key: 'yarn_batch', width: 120 },
{ label: '机号', key: 'machine_number', width: 100 },
{ label: '条数', key: 'roll', width: 80 },
{ label: '重量', key: 'weight', width: 100 },
];
/** 扫码详情区汇总维度(勾选后参与表格分组;带 lock 的维度会随扫码请求传给后端) */
export const SCAN_GROUP_OPTIONS = [ export const SCAN_GROUP_OPTIONS = [
{ key: 'produce_order_no', label: '合同' }, { key: 'produce_order_no', label: '合同' },
{ key: 'grey_fabric_code', label: '布编' }, { key: 'grey_fabric_code', label: '布编' },
@@ -300,56 +296,6 @@ export function buildScanDetailFromContext(context, order) {
}; };
} }
function aggregateScanTableRows(rows, keys) {
const map = new Map();
rows.forEach((row) => {
const groupId = keys.map((k) => row[k] || '').join('|');
const existing = map.get(groupId);
if (!existing) {
map.set(groupId, { ...row, roll: 0, raw_weight: 0 });
}
const agg = map.get(groupId);
agg.roll = Number((agg.roll + Number(row.roll || 0)).toFixed(2));
agg.raw_weight = Number(agg.raw_weight || 0) + Number(row.raw_weight || 0);
});
return Array.from(map.values()).map((row) => ({
...row,
weight: formatWeightKg(row.raw_weight),
}));
}
/** 构建扫码详情区汇总表格数据 */
export function buildScanDetailTableRows(itemData, groupKeys = []) {
const rows = [];
(itemData || []).forEach((item) => {
(item.fine_codes || []).forEach((fc) => {
rows.push({
customer_name: item.customer_name || '',
produce_order_no: item.produce_order_no || item.contract_no || '',
grey_fabric_code: item.grey_fabric_code || '',
grey_fabric_name: item.grey_fabric_name || '',
yarn_batch: item.yarn_batch || '',
machine_number: item.machine_number || '',
volume_number: fc.roll_no || fc.fabric_piece_code || '',
grey_fabric_level_name: item.grey_fabric_level_name || '',
roll: fc.roll || 0,
raw_weight: fc.raw_weight != null ? fc.raw_weight : 0,
});
});
});
if (!rows.length) return [];
const keys = (groupKeys || []).filter(Boolean);
if (!keys.length) {
return aggregateScanTableRows(rows, ['grey_fabric_code', 'grey_fabric_name', 'yarn_batch', 'machine_number']);
}
return aggregateScanTableRows(rows, keys);
}
export function getScanDetailTableWidth(columns) {
const total = (columns || []).reduce((sum, col) => sum + (col.width || 120), 0);
return `${total}rpx`;
}
export function normalizeItem(item) { export function normalizeItem(item) {
const machineStockRaw = item.machine_stock_roll ?? item.machine_stock ?? item.machine_inventory_roll; const machineStockRaw = item.machine_stock_roll ?? item.machine_stock ?? item.machine_inventory_roll;
return { return {
+51
View File
@@ -366,6 +366,56 @@ function playErrorAudio() {
errorAudio.play(); errorAudio.play();
} }
// 坯布出仓等业务的固定中文提示语音
const SPEAK_AUDIO = {
'扫码出仓成功': '/static/audio/scan_out_ok.mp3',
'删除成功': '/static/audio/delete_ok.mp3',
'提交成功': '/static/audio/submit_ok.mp3',
'审核成功': '/static/audio/audit_ok.mp3',
'作废成功': '/static/audio/void_ok.mp3',
};
const speakAudioMap = {};
function stopOtherSpeak(exceptText) {
Object.keys(speakAudioMap).forEach((key) => {
if (key === exceptText || !speakAudioMap[key]) return;
try {
speakAudioMap[key].stop();
} catch (e) {}
});
if (successAudio) {
try {
successAudio.stop();
} catch (e) {}
}
}
function playSpeak(text) {
const src = SPEAK_AUDIO[text];
if (!src) return;
stopOtherSpeak(text);
const prev = speakAudioMap[text];
if (prev) {
try {
prev.stop();
prev.destroy();
} catch (e) {}
speakAudioMap[text] = undefined;
}
const audio = uni.createInnerAudioContext();
audio.autoplay = true;
audio.src = src;
audio.onError((res) => {
console.log('语音播放失败', text, res && res.errMsg);
try {
audio.destroy();
} catch (e) {}
if (speakAudioMap[text] === audio) speakAudioMap[text] = undefined;
});
speakAudioMap[text] = audio;
}
export default { export default {
get apiurl() { get apiurl() {
@@ -378,4 +428,5 @@ export default {
parGetData, parGetData,
playSuccessAudio, playSuccessAudio,
playErrorAudio, playErrorAudio,
playSpeak,
}; };
@@ -1,6 +1,6 @@
<template> <template>
<view> <view>
<view class="fabric-out-sectionTitle">{{ title }}</view> <view v-if="title" class="fabric-out-sectionTitle">{{ title }}</view>
<slot name="before-list" /> <slot name="before-list" />
<view class="fabric-out-detailList"> <view class="fabric-out-detailList">
<view <view
@@ -26,7 +26,7 @@
export default { export default {
props: { props: {
list: { type: Array, default: () => [] }, list: { type: Array, default: () => [] },
title: { type: String, default: '出仓细码' }, title: { type: String, default: '' },
}, },
}; };
</script> </script>
@@ -12,12 +12,14 @@
<text class="fabric-out-scanDetailLabel">客户:</text> <text class="fabric-out-scanDetailLabel">客户:</text>
<text class="fabric-out-scanDetailValue">{{ detail.customer_name || '' }}</text> <text class="fabric-out-scanDetailValue">{{ detail.customer_name || '' }}</text>
</view> </view>
<view class="fabric-out-scanDetailRow fabric-out-scanDetailRow--2col"> <view class="fabric-out-scanDetailRow">
<view class="fabric-out-scanDetailCheck" @click="toggleGroup('produce_order_no')"> <view class="fabric-out-scanDetailCheck" @click="toggleGroup('produce_order_no')">
<checkbox :checked="isGroupChecked('produce_order_no')" /> <checkbox :checked="isGroupChecked('produce_order_no')" />
<text class="fabric-out-scanDetailLabel">生产通知单:</text> <text class="fabric-out-scanDetailLabel">生产通知单:</text>
<text class="fabric-out-scanDetailValue">{{ detail.produce_order_no || '' }}</text> <text class="fabric-out-scanDetailValue">{{ detail.produce_order_no || '' }}</text>
</view> </view>
</view>
<view class="fabric-out-scanDetailRow">
<view class="fabric-out-scanDetailCheck" @click="toggleGroup('grey_fabric_code')"> <view class="fabric-out-scanDetailCheck" @click="toggleGroup('grey_fabric_code')">
<checkbox :checked="isGroupChecked('grey_fabric_code')" /> <checkbox :checked="isGroupChecked('grey_fabric_code')" />
<text class="fabric-out-scanDetailLabel">布编:</text> <text class="fabric-out-scanDetailLabel">布编:</text>
@@ -58,67 +60,16 @@
<text class="fabric-out-scanDetailValue">{{ formatWeight(detail.weight) }}Kg</text> <text class="fabric-out-scanDetailValue">{{ formatWeight(detail.weight) }}Kg</text>
</view> </view>
</view> </view>
<scroll-view scroll-x class="fabric-out-scanDetailTableScroll">
<view class="fabric-out-scanDetailTable" :style="{ width: tableWidth }">
<view class="fabric-out-scanDetailTableRow fabric-out-scanDetailTableHead">
<view
v-for="col in columns"
:key="col.key"
class="fabric-out-scanDetailTableCell"
:style="cellStyle(col)"
>{{ col.label }}</view>
</view>
<view v-if="!tableRows.length" class="fabric-out-scanDetailEmpty">暂无数据~~</view>
<view
v-for="(row, rowIndex) in tableRows"
:key="rowIndex"
class="fabric-out-scanDetailTableRow"
>
<view
v-for="col in columns"
:key="col.key"
class="fabric-out-scanDetailTableCell"
:style="cellStyle(col)"
>{{ formatCell(row, col.key) }}</view>
</view>
</view>
</scroll-view>
</view> </view>
</template> </template>
<script> <script>
import {
SCAN_DETAIL_TABLE_COLUMNS,
getScanDetailTableWidth,
buildScanDetailTableRows,
} from '@/common/storeFabricBusinessOut';
export default { export default {
props: { props: {
detail: { type: Object, default: () => ({}) }, detail: { type: Object, default: () => ({}) },
itemData: { type: Array, default: () => [] },
groupKeys: { type: Array, default: () => [] }, groupKeys: { type: Array, default: () => [] },
}, },
computed: {
columns() {
return SCAN_DETAIL_TABLE_COLUMNS;
},
tableWidth() {
return getScanDetailTableWidth(this.columns);
},
tableRows() {
return buildScanDetailTableRows(this.itemData, this.groupKeys);
},
},
methods: { methods: {
cellStyle(col) {
return {
width: `${col.width}rpx`,
minWidth: `${col.width}rpx`,
maxWidth: `${col.width}rpx`,
};
},
isGroupChecked(key) { isGroupChecked(key) {
return (this.groupKeys || []).includes(key); return (this.groupKeys || []).includes(key);
}, },
@@ -140,11 +91,6 @@ export default {
const n = Number(value || 0); const n = Number(value || 0);
return Number(n.toFixed(2)); return Number(n.toFixed(2));
}, },
formatCell(row, key) {
if (key === 'roll') return this.formatRoll(row.roll);
if (key === 'weight') return this.formatWeight(row.weight);
return row[key] || '';
},
}, },
}; };
</script> </script>
@@ -89,7 +89,6 @@
<FabricOutScanDetail <FabricOutScanDetail
slot="before-list" slot="before-list"
:detail="scanDetail" :detail="scanDetail"
:item-data="form.item_data"
:group-keys="scanGroupKeys" :group-keys="scanGroupKeys"
@group-change="scanGroupKeys = $event" @group-change="scanGroupKeys = $event"
/> />
@@ -43,7 +43,6 @@
<FabricOutScanDetail <FabricOutScanDetail
slot="before-list" slot="before-list"
:detail="scanDetail" :detail="scanDetail"
:item-data="form.item_data"
:group-keys="scanGroupKeys" :group-keys="scanGroupKeys"
@group-change="scanGroupKeys = $event" @group-change="scanGroupKeys = $event"
/> />
@@ -22,6 +22,7 @@ import {
getFormStatusActionButtons, getFormStatusActionButtons,
orderToFormFields, orderToFormFields,
confirmOrderStatusAction, confirmOrderStatusAction,
STATUS_ACTION_VOICE,
DRAFT_AUDIT_STATUS, DRAFT_AUDIT_STATUS,
DRAFT_AUDIT_STATUS_NAME, DRAFT_AUDIT_STATUS_NAME,
getEmptyScanDetail, getEmptyScanDetail,
@@ -242,7 +243,7 @@ export default {
this.applyOrder(order); this.applyOrder(order);
this.headerSaved = true; this.headerSaved = true;
this.syncCanScanFromStatus(order.audit_status); this.syncCanScanFromStatus(order.audit_status);
this.showSuccess('保存成功,可以扫码'); this.showSuccess('保存成功,可以扫码', '提交成功');
this.registerOrderScanBroadcast(); this.registerOrderScanBroadcast();
}) })
.catch((e) => { .catch((e) => {
@@ -330,10 +331,10 @@ export default {
const row = order.item_data.find((i) => (i.group_key || String(i.id)) === this.currentGroupKey); const row = order.item_data.find((i) => (i.group_key || String(i.id)) === this.currentGroupKey);
this.currentFineCodes = row ? [...row.fine_codes] : []; this.currentFineCodes = row ? [...row.fine_codes] : [];
} }
this.BillDataMessage = scanType === 2 ? '删除成功' : '扫描成功'; this.BillDataMessage = scanType === 2 ? '删除成功' : '扫码出仓成功';
this.QRBarCode = ''; this.QRBarCode = '';
if (scanType === 2) this.showSuccess('删除成功'); if (scanType === 2) this.showSuccess('删除成功', '删除成功');
else util.playSuccessAudio(); else util.playSpeak('扫码出仓成功');
}) })
.catch((e) => { .catch((e) => {
uni.hideLoading(); uni.hideLoading();
@@ -377,7 +378,8 @@ export default {
if (!order) return; if (!order) return;
this.applyOrder(order); this.applyOrder(order);
this.syncCanScanFromStatus(order.audit_status); this.syncCanScanFromStatus(order.audit_status);
this.showSuccess('操作成功'); const voice = STATUS_ACTION_VOICE[action] || '';
this.showSuccess(voice || '操作成功', voice);
}), }),
}); });
}, },
@@ -385,8 +387,9 @@ export default {
util.playErrorAudio(); util.playErrorAudio();
uni.showModal({ title: '提示', content: message, showCancel: false }); uni.showModal({ title: '提示', content: message, showCancel: false });
}, },
showSuccess(message) { showSuccess(message, speakText) {
util.playSuccessAudio(); if (speakText) util.playSpeak(speakText);
else util.playSuccessAudio();
uni.showToast({ title: message, icon: 'success' }); uni.showToast({ title: message, icon: 'success' });
}, },
getBillTypeName() { getBillTypeName() {
@@ -125,34 +125,6 @@
color: #333; color: #333;
word-break: break-all; word-break: break-all;
} }
.fabric-out-scanDetailTableScroll {
width: 100%;
}
.fabric-out-scanDetailTable {
min-width: 100%;
}
.fabric-out-scanDetailTableRow {
display: flex;
border-bottom: 1rpx solid #f0f0f0;
}
.fabric-out-scanDetailTableHead {
background: #f5f5f5;
font-weight: bold;
}
.fabric-out-scanDetailTableCell {
padding: 14rpx 8rpx;
font-size: 32rpx;
color: #333;
text-align: center;
box-sizing: border-box;
word-break: break-all;
}
.fabric-out-scanDetailEmpty {
padding: 24rpx;
text-align: center;
font-size: 36rpx;
color: #999;
}
.fabric-out-submitView { .fabric-out-submitView {
width: 100%; width: 100%;
padding: 16rpx 26rpx 26rpx; padding: 16rpx 26rpx 26rpx;
@@ -53,6 +53,7 @@ import {
getViewStatusActionButtons, getViewStatusActionButtons,
confirmOrderStatusAction, confirmOrderStatusAction,
formatDisplayTime, formatDisplayTime,
STATUS_ACTION_VOICE,
} from '@/common/storeFabricBusinessOut'; } from '@/common/storeFabricBusinessOut';
export default { export default {
@@ -123,8 +124,10 @@ export default {
uni.hideLoading(); uni.hideLoading();
const order = normalizeOrder(res); const order = normalizeOrder(res);
if (order) this.order = order; if (order) this.order = order;
util.playSuccessAudio(); const voice = STATUS_ACTION_VOICE[action] || '';
uni.showToast({ title: '操作成功', icon: 'success' }); if (voice) util.playSpeak(voice);
else util.playSuccessAudio();
uni.showToast({ title: voice || '操作成功', icon: 'success' });
}), }),
}); });
}, },
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.