<template>
|
<view class="account-detail">
|
<PageHeader :title="pageTitle" @back="goBack" />
|
|
<view class="form-container">
|
<up-form ref="formRef" :model="form" label-width="110" input-align="right" error-message-align="right">
|
<u-cell-group title="产品信息" class="form-section">
|
<view class="section-tools">
|
<up-button type="primary" size="small" text="新增产品" :disabled="isEditMode" @click="addProduct" />
|
</view>
|
<view v-if="form.products.length === 0" class="empty-text"><text>暂无产品</text></view>
|
<view v-else class="product-list">
|
<view v-for="(product, index) in form.products" :key="product.uid || index" class="product-card">
|
<view class="product-header">
|
<text class="product-title">产品 {{ index + 1 }}</text>
|
<up-icon name="trash" color="#ee0a24" size="18" @click="removeProduct(index)"></up-icon>
|
</view>
|
<up-divider></up-divider>
|
<view class="product-body">
|
<up-form-item label="产品">
|
<up-input v-model="product.product" placeholder="请选择产品" readonly @click="openProductPicker(index)" />
|
<template #right><up-icon name="arrow-right" @click="openProductPicker(index)"></up-icon></template>
|
</up-form-item>
|
<up-form-item label="规格">
|
<up-input v-model="product.specification" placeholder="请选择规格" readonly @click="openModelPicker(index)" />
|
<template #right><up-icon name="arrow-right" @click="openModelPicker(index)"></up-icon></template>
|
</up-form-item>
|
<up-form-item label="单位"><up-input v-model="product.unit" placeholder="请输入单位" clearable /></up-form-item>
|
<up-form-item label="纸张"><up-input v-model="product.paper" placeholder="请输入纸张" clearable /></up-form-item>
|
<up-form-item label="定量"><up-input v-model="product.paperWeight" placeholder="请输入定量" clearable /></up-form-item>
|
<up-form-item label="数量">
|
<up-input v-model="product.quantity" type="number" placeholder="请输入数量" clearable @blur="calculateAmount(product)" />
|
</up-form-item>
|
<up-form-item label="单价">
|
<up-input v-model="product.unitPrice" type="number" placeholder="请输入单价" clearable @blur="calculateAmount(product)" />
|
</up-form-item>
|
<up-form-item label="印版费">
|
<up-input v-model="product.printingFee" type="number" placeholder="请输入印版费" clearable @blur="syncTotalAmount" />
|
</up-form-item>
|
<up-form-item label="刀版费">
|
<up-input v-model="product.dieCuttingFee" type="number" placeholder="请输入刀版费" clearable @blur="syncTotalAmount" />
|
</up-form-item>
|
<up-form-item label="磨具费">
|
<up-input v-model="product.grindingFee" type="number" placeholder="请输入磨具费" clearable @blur="syncTotalAmount" />
|
</up-form-item>
|
<up-form-item label="金额">
|
<up-input :model-value="formatAmount(product.amount)" disabled placeholder="自动计算(数量*单价)" />
|
</up-form-item>
|
</view>
|
</view>
|
</view>
|
</u-cell-group>
|
|
<u-cell-group title="备注信息" class="form-section">
|
<up-form-item label="备注">
|
<up-textarea v-model="form.remark" placeholder="请输入备注(选填)" auto-height />
|
</up-form-item>
|
</u-cell-group>
|
|
<u-cell-group title="汇总" class="form-section">
|
<up-form-item label="报价总额">
|
<up-input :model-value="formatAmount(totalAmount)" disabled placeholder="自动汇总" />
|
</up-form-item>
|
<view class="summary-tip">总额规则:单价 + 印版费 + 刀版费 + 磨具费(按产品逐行求和)</view>
|
</u-cell-group>
|
</up-form>
|
</view>
|
|
<FooterButtons :loading="loading" confirmText="保存" @cancel="goBack" @confirm="handleSubmit" />
|
|
<up-action-sheet :show="showProductSheet" title="选择产品" :actions="productActions" @select="onSelectProduct" @close="showProductSheet = false" />
|
<up-action-sheet :show="showModelSheet" title="选择规格" :actions="modelActions" @select="onSelectModel" @close="showModelSheet = false" />
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, onMounted, ref } from "vue";
|
import { onLoad } from "@dcloudio/uni-app";
|
import FooterButtons from "@/components/FooterButtons.vue";
|
import PageHeader from "@/components/PageHeader.vue";
|
import { modelList, productTreeList } from "@/api/basicData/product";
|
import { addOrUpdateQuotationProduct, editQuotationProduct } from "@/api/salesManagement/salesQuotationProduct";
|
|
const formRef = ref();
|
const loading = ref(false);
|
const quotationId = ref("");
|
const showProductSheet = ref(false);
|
const showModelSheet = ref(false);
|
const currentProductIndex = ref(-1);
|
const productList = ref([]);
|
const modelActions = ref([]);
|
|
let uidSeed = 1;
|
const form = ref({
|
id: undefined,
|
remark: "",
|
products: [],
|
totalAmount: 0,
|
});
|
|
const pageTitle = computed(() => (quotationId.value ? "编辑报价" : "新增报价"));
|
const isEditMode = computed(() => Boolean(quotationId.value));
|
const productActions = computed(() => productList.value.map(item => ({ name: item.label, value: item.value, label: item.label })));
|
const totalAmount = computed(() => calcTotalAmountFromProducts(form.value.products));
|
|
const createEmptyProduct = () => ({
|
uid: `p_${uidSeed++}`,
|
id: "",
|
salesQuotationId: "",
|
productId: "",
|
product: "",
|
specificationId: "",
|
specification: "",
|
unit: "",
|
paper: "",
|
paperWeight: "",
|
quantity: 1,
|
unitPrice: 0,
|
printingFee: 0,
|
dieCuttingFee: 0,
|
grindingFee: 0,
|
amount: 0,
|
modelOptions: [],
|
});
|
|
const flattenProductTree = nodes => {
|
const result = [];
|
const walk = list => {
|
(list || []).forEach(item => {
|
if (item.children && item.children.length) walk(item.children);
|
else result.push({ label: item.label || item.productName || "", value: item.id || item.value });
|
});
|
};
|
walk(nodes);
|
return result;
|
};
|
|
const findProductIdByLabel = label => {
|
if (!label) return "";
|
const hit = (productList.value || []).find(item => item.label === label);
|
return hit?.value || "";
|
};
|
|
const formatAmount = amount => `¥${Number(amount || 0).toFixed(2)}`;
|
const goBack = () => uni.navigateBack();
|
|
const calcTotalAmountFromProducts = products =>
|
Number(
|
(products || [])
|
.reduce((sum, item) => {
|
const unitPrice = Number(item?.unitPrice || 0);
|
const printingFee = Number(item?.printingFee || 0);
|
const dieCuttingFee = Number(item?.dieCuttingFee || 0);
|
const grindingFee = Number(item?.grindingFee || 0);
|
return sum + unitPrice + printingFee + dieCuttingFee + grindingFee;
|
}, 0)
|
.toFixed(2)
|
);
|
|
const syncTotalAmount = () => {
|
form.value.totalAmount = totalAmount.value;
|
};
|
|
const calculateAmount = product => {
|
product.amount = Number((Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2));
|
syncTotalAmount();
|
};
|
|
const addProduct = () => {
|
if (isEditMode.value) {
|
uni.showToast({ title: "编辑模式下不允许新增产品", icon: "none" });
|
return;
|
}
|
form.value.products.push(createEmptyProduct());
|
};
|
|
const removeProduct = index => {
|
form.value.products.splice(index, 1);
|
syncTotalAmount();
|
};
|
|
const fetchModelOptions = async (productId, product) => {
|
const rows = await modelList({ id: productId }).catch(() => []);
|
product.modelOptions = Array.isArray(rows) ? rows : [];
|
};
|
|
const openProductPicker = index => {
|
currentProductIndex.value = index;
|
showProductSheet.value = true;
|
};
|
|
const openModelPicker = index => {
|
currentProductIndex.value = index;
|
const current = form.value.products[index];
|
if (!current?.productId) {
|
uni.showToast({ title: "请先选择产品", icon: "none" });
|
return;
|
}
|
modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit }));
|
if (!modelActions.value.length) {
|
uni.showToast({ title: "暂无规格数据", icon: "none" });
|
return;
|
}
|
showModelSheet.value = true;
|
};
|
|
const onSelectProduct = action => {
|
const current = form.value.products[currentProductIndex.value];
|
if (!current) return;
|
current.productId = action.value;
|
current.product = action.label;
|
current.specificationId = "";
|
current.specification = "";
|
current.unit = "";
|
current.modelOptions = [];
|
showProductSheet.value = false;
|
fetchModelOptions(action.value, current);
|
};
|
|
const onSelectModel = action => {
|
const current = form.value.products[currentProductIndex.value];
|
if (!current) return;
|
current.specificationId = action.value;
|
current.specification = action.name;
|
current.unit = action.unit || current.unit;
|
showModelSheet.value = false;
|
};
|
|
const fetchProductOptions = async () => {
|
const productTree = await productTreeList().catch(() => []);
|
productList.value = flattenProductTree(Array.isArray(productTree) ? productTree : productTree?.data || []);
|
};
|
|
const normalizeProductRows = async rows => {
|
const normalized = await Promise.all(
|
(Array.isArray(rows) ? rows : []).map(async item => {
|
const row = {
|
uid: `p_${uidSeed++}`,
|
id: item.id || "",
|
salesQuotationId: item.salesQuotationId || "",
|
productId: item.productId || "",
|
product: item.product || item.productName || "",
|
specificationId: item.specificationId || "",
|
specification: item.specification || "",
|
unit: item.unit || "",
|
paper: item.paper || "",
|
paperWeight: item.paperWeight || "",
|
quantity: Number(item.quantity || 1),
|
unitPrice: Number(item.unitPrice || 0),
|
printingFee: Number(item.printingFee || 0),
|
dieCuttingFee: Number(item.dieCuttingFee || 0),
|
grindingFee: Number(item.grindingFee || 0),
|
amount: Number(item.amount || Number(item.quantity || 0) * Number(item.unitPrice || 0)),
|
modelOptions: [],
|
};
|
if (row.productId) {
|
await fetchModelOptions(row.productId, row);
|
if (!row.specificationId && row.specification) {
|
const matchedModel = (row.modelOptions || []).find(model => model.model === row.specification);
|
if (matchedModel) {
|
row.specificationId = matchedModel.id;
|
if (!row.unit) row.unit = matchedModel.unit || "";
|
}
|
}
|
}
|
return row;
|
})
|
);
|
form.value.products = normalized;
|
};
|
|
const loadEditFromStorage = async () => {
|
if (!quotationId.value) return;
|
const cached = uni.getStorageSync("salesQuotationEdit");
|
if (!cached || typeof cached !== "object") return;
|
if (cached.id && String(cached.id) !== String(quotationId.value)) return;
|
|
const data = cached;
|
form.value = {
|
...form.value,
|
id: data.id || form.value.id,
|
remark: data.remark || "",
|
};
|
|
const rows = Array.isArray(data.products) && data.products.length ? data.products : [data];
|
const normalizedRows = rows.map(item => ({
|
...item,
|
productId: item.productId || findProductIdByLabel(item.product || item.productName || ""),
|
}));
|
await normalizeProductRows(normalizedRows);
|
syncTotalAmount();
|
};
|
|
const validateProducts = () => {
|
if (!form.value.products.length) {
|
uni.showToast({ title: "请至少添加一个产品", icon: "none" });
|
return false;
|
}
|
const invalid = form.value.products.some(item => !item.productId || !item.specificationId || !item.unit || !Number(item.unitPrice || 0));
|
if (invalid) {
|
uni.showToast({ title: "请完善产品信息", icon: "none" });
|
return false;
|
}
|
return true;
|
};
|
|
const buildProductPayload = item => {
|
const quantity = Number(item?.quantity || 0);
|
const unitPrice = Number(item?.unitPrice || 0);
|
const printingFee = Number(item?.printingFee || 0);
|
const dieCuttingFee = Number(item?.dieCuttingFee || 0);
|
const grindingFee = Number(item?.grindingFee || 0);
|
return {
|
id: item?.id || undefined,
|
salesQuotationId: item?.salesQuotationId || null,
|
product: item?.product || "",
|
specification: item?.specification || "",
|
unit: item?.unit || "",
|
paper: item?.paper || "",
|
paperWeight: item?.paperWeight || "",
|
unitPrice,
|
printingFee,
|
dieCuttingFee,
|
grindingFee,
|
quantity,
|
amount: Number(item?.amount ?? quantity * unitPrice),
|
remark: form.value.remark || "",
|
};
|
};
|
|
const handleSubmit = async () => {
|
if (!validateProducts()) return;
|
|
loading.value = true;
|
if (quotationId.value) {
|
const editingItem = form.value.products[0] || {};
|
const payload = buildProductPayload({
|
...editingItem,
|
id: editingItem.id || quotationId.value,
|
});
|
editQuotationProduct(payload)
|
.then(() => {
|
uni.showToast({ title: "保存成功", icon: "success" });
|
setTimeout(() => uni.navigateBack(), 300);
|
})
|
.catch(() => {
|
uni.showToast({ title: "保存失败", icon: "error" });
|
})
|
.finally(() => {
|
loading.value = false;
|
});
|
return;
|
}
|
|
const payloadList = form.value.products.map(item => buildProductPayload(item));
|
addOrUpdateQuotationProduct(payloadList)
|
.then(() => {
|
uni.showToast({ title: "保存成功", icon: "success" });
|
setTimeout(() => uni.navigateBack(), 300);
|
})
|
.catch(() => {
|
uni.showToast({ title: "保存失败", icon: "error" });
|
})
|
.finally(() => {
|
loading.value = false;
|
});
|
};
|
|
onLoad(options => {
|
if (options?.id) {
|
quotationId.value = options.id;
|
form.value.id = options.id;
|
} else {
|
form.value.products = [];
|
}
|
});
|
|
onMounted(async () => {
|
await fetchProductOptions();
|
if (quotationId.value) await loadEditFromStorage();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/static/scss/form-common.scss";
|
|
.form-container {
|
padding: 12px 12px 0;
|
}
|
|
.form-section {
|
margin-bottom: 12px;
|
border-radius: 12px;
|
overflow: hidden;
|
box-shadow: 0 2px 10px rgba(15, 35, 95, 0.05);
|
}
|
|
.section-tools {
|
display: flex;
|
justify-content: flex-end;
|
padding: 12px 12px 0;
|
}
|
|
.product-list {
|
padding: 12px;
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
|
.product-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.product-title {
|
font-size: 14px;
|
font-weight: 600;
|
color: #22324d;
|
}
|
|
.product-card {
|
background: #fff;
|
border-radius: 12px;
|
padding: 0 12px 12px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
}
|
|
.product-header {
|
padding: 12px 0;
|
}
|
|
.empty-text {
|
padding: 16px 12px;
|
color: #999;
|
font-size: 14px;
|
}
|
|
.summary-tip {
|
padding: 0 24rpx 24rpx;
|
color: #909399;
|
font-size: 12px;
|
line-height: 1.6;
|
}
|
|
:deep(.u-cell-group__title) {
|
padding: 14px 18px 10px !important;
|
font-size: 15px !important;
|
font-weight: 600 !important;
|
color: #22324d !important;
|
background: #f8fbff !important;
|
}
|
</style>
|