<template>
|
<view class="form-page">
|
<CardTitle :title="pageTitle" :hideAction="false" />
|
|
<!-- 源盘:上一步已扫码领用,这里只做展示 -->
|
<view class="section">
|
<view class="section-title">源盘(成品)</view>
|
<view class="card source-card">
|
<view class="kv">
|
<text class="kv-label">盘号</text>
|
<text class="kv-value">{{ source.batchCode || "-" }}</text>
|
</view>
|
<view class="kv">
|
<text class="kv-label">规格型号</text>
|
<text class="kv-value">{{ source.model || "-" }}</text>
|
</view>
|
<view class="kv">
|
<text class="kv-label">长度(m)</text>
|
<text class="kv-value">{{ source.currentLength ?? "-" }}</text>
|
</view>
|
<view class="kv">
|
<text class="kv-label">重量(kg)</text>
|
<text class="kv-value">{{ source.currentWeight ?? "-" }}</text>
|
</view>
|
<view class="kv">
|
<text class="kv-label">米重(kg/km)</text>
|
<text class="kv-value">{{ source.meterWeight ?? "-" }}</text>
|
</view>
|
<view class="kv">
|
<text class="kv-label">状态</text>
|
<text class="kv-value ok">
|
{{ source.qualityStatus || "-" }} · {{ source.enableStatus || "-" }}
|
</text>
|
</view>
|
</view>
|
</view>
|
|
<!-- 子盘 / 目标盘 -->
|
<view class="section">
|
<view class="section-title">{{ isRewind ? "目标盘" : "子盘" }}</view>
|
|
<view v-for="(target, index) in targets" :key="index" class="card target-card">
|
<view class="row">
|
<text class="row-label">{{ isRewind ? "目标盘" : `子盘${index + 1}` }}</text>
|
<view class="row-value">
|
<wd-input v-model="target.length" type="number" no-border placeholder="请输入长度(m)" />
|
</view>
|
<text class="row-unit">米</text>
|
<view v-if="!isRewind" class="row-btns">
|
<wd-button size="small" plain :round="false" type="error" @click="removeTarget(index)">
|
删除
|
</wd-button>
|
</view>
|
</view>
|
|
<view class="row">
|
<text class="row-label">领用盘具</text>
|
<view class="row-value">
|
<wd-input
|
v-model="target.codeInput"
|
no-border
|
clearable
|
:placeholder="dishPlaceholder"
|
/>
|
</view>
|
</view>
|
<view class="row-btns">
|
<wd-button size="small" plain :round="false" @click="scanDish(index)">
|
扫码选择盘具
|
</wd-button>
|
<wd-button size="small" plain :round="false" @click="searchDish(index)">查询</wd-button>
|
</view>
|
|
<view v-if="target.dishCode" class="kv">
|
<text class="kv-label">盘具规格</text>
|
<text class="kv-value">
|
{{ target.dishModel || "-" }}
|
<text v-if="target.dishWeight !== undefined && target.dishWeight !== null">
|
· {{ target.dishWeight }}kg
|
</text>
|
</text>
|
</view>
|
<view class="kv">
|
<text class="kv-label">{{ isRewind ? "目标盘号" : "子盘盘号" }}</text>
|
<view class="chip-switch">
|
<view
|
v-for="opt in batchCodeOptions"
|
:key="String(opt.value)"
|
class="chip-item"
|
:class="{ 'chip-item-active': target.useCurrentBatchCode === opt.value }"
|
@click="selectBatchCodeMode(index, opt.value)"
|
>
|
{{ opt.label }}
|
</view>
|
</view>
|
</view>
|
<view class="kv">
|
<text class="kv-label">实际盘号</text>
|
<text class="kv-value">{{ targetBatchCode(index) || "提交后自动生成" }}</text>
|
</view>
|
</view>
|
|
<view v-if="!isRewind" class="add-target" @click="addTarget">+ 添加子盘</view>
|
</view>
|
|
<!-- 操作人 -->
|
<view class="section">
|
<view class="section-title">操作人</view>
|
<view class="card">
|
<wd-select-picker
|
v-model="operatorName"
|
type="radio"
|
:columns="userOptions"
|
label="操作人"
|
label-width="100px"
|
filterable
|
filter-placeholder="搜索人员"
|
placeholder="请选择操作人"
|
:loading="userLoading"
|
:z-index="30"
|
/>
|
</view>
|
</view>
|
|
<!-- 备注 -->
|
<view class="section">
|
<view class="section-title">备注</view>
|
<view class="card">
|
<wd-textarea v-model="remark" placeholder="可选备注" :maxlength="200" show-word-limit />
|
</view>
|
</view>
|
|
<view class="footer">
|
<wd-button block :round="false" :loading="submitting" @click="submit">
|
<text class="text-[#fff]">提交</text>
|
</wd-button>
|
</view>
|
|
<Scan ref="scanRef" emitName="scanInventoryDish" />
|
<wd-toast />
|
</view>
|
</template>
|
|
<script setup lang="ts">
|
import CardTitle from "@/components/card-title/index.vue";
|
import Scan from "@/components/scan/index.vue";
|
import { useToast } from "wot-design-uni";
|
import { onLoad, onUnload, onShow, onHide } from "@dcloudio/uni-app";
|
import InventoryOperationApi from "@/api/product/inventoryOperation";
|
import UserAPI from "@/api/system/user";
|
import { useUserStore } from "@/store/modules/user";
|
import { MOCK_BOBBIN, MOCK_ENABLED } from "./mock";
|
import {
|
DISH_CODE_KEYS,
|
MATERIAL_TYPE_FG,
|
OP_SPLIT,
|
OP_STRAND_REWIND,
|
SOURCE_STORAGE_KEY,
|
findBobbin,
|
isBobbinReceived,
|
parseScanCode,
|
pickValue,
|
} from "./shared";
|
|
interface TargetItem {
|
/** 目标长度(m) */
|
length: any;
|
/** 领用盘具编号(盘具称重台账的称重编号) */
|
dishCode: string;
|
dishModel: string;
|
dishWeight: any;
|
/** 盘具编号输入框 */
|
codeInput: string;
|
/** 该盘是否沿用原盘号(拆盘时最多一盘可以沿用) */
|
useCurrentBatchCode: boolean;
|
}
|
|
const createTarget = (length: any = "", useCurrentBatchCode = false): TargetItem => ({
|
length,
|
dishCode: "",
|
dishModel: "",
|
dishWeight: undefined,
|
codeInput: "",
|
useCurrentBatchCode,
|
});
|
|
const toast = useToast();
|
const scanRef = ref();
|
const operationType = ref(OP_SPLIT);
|
const isRewind = computed(() => operationType.value === OP_STRAND_REWIND);
|
const pageTitle = computed(() => (isRewind.value ? "绞线复绕" : "成品拆盘"));
|
|
/** 源盘:来自上一步扫码领用 */
|
const source = ref<any>({});
|
// 第一个盘默认沿用原盘号(复绕整盘沿用);后续添加的子盘默认生成新盘号
|
const targets = ref<TargetItem[]>([createTarget("", true)]);
|
/** 盘号模式:每个盘单独选 */
|
const batchCodeOptions = [
|
{ label: "沿用原盘号", value: true },
|
{ label: "生成新盘号", value: false },
|
];
|
const remark = ref("");
|
const submitting = ref(false);
|
const isPageVisible = ref(true);
|
|
/* ==================== 操作人 ==================== */
|
const userStore = useUserStore();
|
/** 操作人:默认当前登录人,可改成其它人员 */
|
const operatorName = ref("");
|
const userOptions = ref<{ label: string; value: string }[]>([]);
|
const userLoading = ref(false);
|
|
const loadUserOptions = async () => {
|
if (userLoading.value) return;
|
userLoading.value = true;
|
try {
|
const { code, data } = await UserAPI.getUserSelect();
|
if (code === 200) {
|
const names = (data || [])
|
.map((item: any) => String(item?.nickName || item?.nickname || item?.userName || "").trim())
|
.filter(Boolean);
|
userOptions.value = Array.from(new Set<string>(names)).map((name) => ({
|
label: name,
|
value: name,
|
}));
|
}
|
} catch (error) {
|
// 人员接口异常时不阻塞办理,操作人仍保留当前登录人
|
} finally {
|
userLoading.value = false;
|
}
|
};
|
|
/** 模拟模式下默认带出一条盘具,输入框里给出默认盘具编号提示 */
|
const dishPlaceholder = computed(() =>
|
MOCK_ENABLED ? `扫码或输入盘具编号,如 ${MOCK_BOBBIN.weighingCode}` : "扫码或输入盘具编号"
|
);
|
|
/** 扫码目标:某个目标盘/子盘的领用盘具 */
|
const scanDishIndex = ref<number | null>(null);
|
|
/* ==================== 领用盘具 ==================== */
|
const applyDishCode = async (index: number, code: string, options: { silent?: boolean } = {}) => {
|
const target = targets.value[index];
|
if (!target) return false;
|
|
if (targets.value.some((item, i) => i !== index && String(item.dishCode ?? "") === code)) {
|
toast.error("该盘具已被本单其它盘使用,请更换盘具");
|
return false;
|
}
|
|
const { record, mocked } = await findBobbin(code);
|
if (!record) {
|
toast.error("盘具称重台账中未找到该盘具,请先在盘具称重台账中称重");
|
return false;
|
}
|
if (isBobbinReceived(record)) {
|
toast.error("该盘具已被领用,请更换盘具");
|
return false;
|
}
|
|
target.dishCode = String(record.weighingCode ?? code).trim();
|
target.dishModel = record.specificationModel ?? "";
|
target.dishWeight = record.weight;
|
target.codeInput = target.dishCode;
|
if (!options.silent) {
|
toast.success(mocked ? "未取到真实数据,使用模拟盘具数据" : "盘具已选择");
|
}
|
return true;
|
};
|
|
const searchDish = async (index: number) => {
|
const code = String(targets.value[index]?.codeInput ?? "").trim();
|
if (!code) {
|
toast.error("请输入或扫描盘具编号");
|
return;
|
}
|
try {
|
await applyDishCode(index, code);
|
} catch (error) {
|
// 请求层已统一提示
|
}
|
};
|
|
const scanDish = (index: number) => {
|
scanDishIndex.value = index;
|
// #ifdef APP-PLUS
|
scanRef.value?.triggerScan();
|
// #endif
|
// #ifndef APP-PLUS
|
toast.warning("当前环境不支持硬件扫码,请用输入框手动查询盘具编号");
|
// #endif
|
};
|
|
/* ==================== 目标盘 ==================== */
|
/** 该盘盘号:选了「沿用原盘号」就用源盘盘号,否则提交空值由后端生成新盘号 */
|
const targetBatchCode = (index: number) => {
|
const target = targets.value[index];
|
return target?.useCurrentBatchCode ? source.value?.batchCode || "" : "";
|
};
|
|
/** 沿用原盘号最多只能有一盘:某一盘选了,其它盘自动改回生成新盘号 */
|
const selectBatchCodeMode = (index: number, value: boolean) => {
|
const target = targets.value[index];
|
if (!target) return;
|
target.useCurrentBatchCode = value;
|
if (!value) return;
|
targets.value.forEach((item, i) => {
|
if (i !== index) item.useCurrentBatchCode = false;
|
});
|
};
|
|
const addTarget = () => {
|
// 新增的子盘默认「生成新盘号」
|
targets.value.push(createTarget());
|
};
|
|
const removeTarget = (index: number) => {
|
targets.value.splice(index, 1);
|
if (targets.value.length === 0) {
|
targets.value.push(createTarget("", true));
|
}
|
};
|
|
/* ==================== 扫码回调(只处理盘具) ==================== */
|
const getScanCode = async (payload: any) => {
|
if (!isPageVisible.value) return;
|
const index = scanDishIndex.value;
|
if (index === null) return;
|
|
const parsed = parseScanCode(payload);
|
if (!parsed) {
|
toast.error("扫码内容为空,请重新扫码");
|
return;
|
}
|
const code = pickValue(parsed, DISH_CODE_KEYS);
|
if (!code) {
|
toast.error("二维码无法识别盘具编号,请更换二维码");
|
return;
|
}
|
|
try {
|
await applyDishCode(index, code);
|
} catch (error) {
|
// 请求层已统一提示
|
} finally {
|
scanDishIndex.value = null;
|
}
|
};
|
|
/* ==================== 提交 ==================== */
|
const submit = async () => {
|
if (!source.value?.batchCode) {
|
toast.error("未获取到源盘信息,请返回重新扫码");
|
return;
|
}
|
if (targets.value.length === 0) {
|
toast.error(isRewind.value ? "请添加目标盘" : "请至少添加一个子盘");
|
return;
|
}
|
if (isRewind.value && targets.value.length > 1) {
|
toast.error("绞线复绕只生成一盘");
|
return;
|
}
|
|
for (let i = 0; i < targets.value.length; i++) {
|
const target = targets.value[i];
|
const label = isRewind.value ? "目标盘" : `子盘${i + 1}`;
|
if (!target.length || Number(target.length) <= 0) {
|
toast.error(`${label}:请输入长度`);
|
return;
|
}
|
// 每一个新盘(子盘/目标盘)都必须选择领用盘具
|
if (!target.dishCode) {
|
toast.error(`${label}:请扫码选择领用盘具`);
|
return;
|
}
|
}
|
|
const totalLength = targets.value.reduce((sum, item) => sum + Number(item.length || 0), 0);
|
const sourceLength = Number(source.value?.currentLength || 0);
|
if (sourceLength > 0 && totalLength > sourceLength) {
|
toast.error(
|
isRewind.value
|
? `目标盘长度(${totalLength})不能超过源盘长度(${sourceLength})`
|
: `子盘长度之和(${totalLength})超过源盘长度(${sourceLength})`
|
);
|
return;
|
}
|
|
const sources = [
|
{
|
batchCode: source.value.batchCode,
|
materialType: MATERIAL_TYPE_FG,
|
model: source.value.model,
|
length: sourceLength || undefined,
|
weight: source.value.currentWeight || undefined,
|
meterWeight: source.value.meterWeight,
|
sortOrder: 1,
|
},
|
];
|
const targetDtos = targets.value.map((item, index) => ({
|
// 单盘可选沿用原盘号,其余提交空值由后端生成新盘号
|
batchCode: targetBatchCode(index),
|
// 领用的盘具编号
|
dishCode: String(item.dishCode ?? "").trim(),
|
materialType: MATERIAL_TYPE_FG,
|
model: source.value.model,
|
length: Number(item.length),
|
meterWeight: source.value.meterWeight,
|
sortOrder: index + 1,
|
}));
|
|
submitting.value = true;
|
try {
|
const { code, msg } = await InventoryOperationApi.createOperation({
|
operationType: operationType.value,
|
remark: remark.value,
|
// 操作人(人员选择,默认当前登录人)
|
createUserName: operatorName.value,
|
sources,
|
targets: targetDtos,
|
});
|
if (code === 200) {
|
toast.success("提交成功");
|
uni.removeStorageSync(SOURCE_STORAGE_KEY);
|
setTimeout(() => uni.navigateBack(), 600);
|
} else {
|
toast.error(msg || "提交失败");
|
}
|
} catch (error) {
|
// 请求层已统一提示
|
} finally {
|
submitting.value = false;
|
}
|
};
|
|
onLoad(async (options: any) => {
|
operationType.value = options?.type === OP_STRAND_REWIND ? OP_STRAND_REWIND : OP_SPLIT;
|
|
// 操作人默认当前登录人,人员列表异步加载后可改选
|
const info: any = userStore.userInfo;
|
operatorName.value = info?.nickname || info?.nickName || info?.userName || "";
|
loadUserOptions();
|
|
const cached = uni.getStorageSync(SOURCE_STORAGE_KEY);
|
if (cached && cached.batchCode) {
|
source.value = { ...cached };
|
// 绞线复绕是整盘复绕:目标盘长度默认与源盘一致
|
if (isRewind.value) {
|
targets.value[0].length = cached.currentLength ?? "";
|
}
|
// 模拟模式下默认带出一条盘具数据,方便直接跑通提交
|
if (MOCK_ENABLED && MOCK_BOBBIN.weighingCode) {
|
await applyDishCode(0, MOCK_BOBBIN.weighingCode, { silent: true });
|
}
|
} else {
|
toast.error("未获取到源盘信息,请返回重新扫码领用");
|
setTimeout(() => uni.navigateBack(), 1000);
|
}
|
|
uni.$off("scanInventoryDish", getScanCode);
|
uni.$on("scanInventoryDish", getScanCode);
|
});
|
|
onShow(() => {
|
isPageVisible.value = true;
|
});
|
|
onHide(() => {
|
isPageVisible.value = false;
|
});
|
|
onUnload(() => {
|
uni.$off("scanInventoryDish", getScanCode);
|
isPageVisible.value = false;
|
// 离开办理页即清掉源盘缓存,下次从选类型重新开始
|
uni.removeStorageSync(SOURCE_STORAGE_KEY);
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.form-page {
|
min-height: 100vh;
|
padding: 12px 12px 96px;
|
background: #f3f9f8;
|
}
|
|
.section {
|
margin-bottom: 12px;
|
}
|
|
.section-title {
|
margin-bottom: 8px;
|
font-size: 14px;
|
font-weight: 600;
|
color: #252525;
|
}
|
|
.card {
|
padding: 12px;
|
background: #ffffff;
|
border: 1px solid #e6e6e6;
|
border-radius: 6px;
|
}
|
|
.source-card {
|
border-color: #0d867f;
|
}
|
|
.target-card {
|
margin-bottom: 10px;
|
border-color: #cfe8e5;
|
}
|
|
.row {
|
display: flex;
|
align-items: center;
|
min-height: 40px;
|
}
|
|
.row-label {
|
flex-shrink: 0;
|
width: 72px;
|
font-size: 13px;
|
color: #646874;
|
}
|
|
.row-value {
|
flex: 1;
|
min-width: 0;
|
}
|
|
.row-unit {
|
margin-left: 4px;
|
font-size: 13px;
|
color: #646874;
|
}
|
|
.row-btns {
|
display: flex;
|
justify-content: flex-end;
|
margin-top: 6px;
|
|
:deep(.wd-button) {
|
margin-left: 8px;
|
}
|
}
|
|
.kv {
|
display: flex;
|
align-items: center;
|
margin-top: 6px;
|
font-size: 13px;
|
}
|
|
.kv-label {
|
width: 88px;
|
color: #646874;
|
}
|
|
.kv-value {
|
flex: 1;
|
color: #252525;
|
}
|
|
.ok {
|
color: #0d867f;
|
}
|
|
.batch-code-mode {
|
display: flex;
|
align-items: center;
|
margin-bottom: 10px;
|
}
|
|
.chip-switch {
|
display: flex;
|
align-items: center;
|
}
|
|
.chip-item {
|
margin-right: 8px;
|
padding: 5px 14px;
|
font-size: 13px;
|
line-height: 20px;
|
color: #0d867f;
|
background: #ffffff;
|
border: 1px solid #0d867f;
|
border-radius: 16px;
|
|
&:last-child {
|
margin-right: 0;
|
}
|
}
|
|
.chip-item-active {
|
color: #ffffff;
|
background: #0d867f;
|
}
|
|
.mode-hint {
|
margin-bottom: 10px;
|
font-size: 12px;
|
color: #8a8a8a;
|
}
|
|
.add-target {
|
height: 36px;
|
font-size: 14px;
|
line-height: 36px;
|
color: #0d867f;
|
text-align: center;
|
border: 1px dashed #0d867f;
|
border-radius: 6px;
|
}
|
|
.footer {
|
position: fixed;
|
right: 0;
|
bottom: 0;
|
left: 0;
|
z-index: 20;
|
padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
|
background: #ffffff;
|
border-top: 1px solid #eeeeee;
|
box-shadow: 0 -2px 10px rgb(0 0 0 / 6%);
|
|
:deep(.wd-button) {
|
--wot-button-primary-bg-color: #0d867f;
|
}
|
}
|
</style>
|