| | |
| | | <template> |
| | | <view class="account-detail"> |
| | | <PageHeader :title="pageTitle" @back="goBack" /> |
| | | |
| | | <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"> |
| | | <up-form ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="110" |
| | | input-align="right" |
| | | error-message-align="right"> |
| | | <u-cell-group title="基础信息" |
| | | class="form-section"> |
| | | <up-form-item label="客户名称" |
| | | prop="customer" |
| | | required> |
| | | <up-input v-model="form.customer" |
| | | placeholder="请选择客户" |
| | | readonly |
| | | @click="showCustomerSheet = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showCustomerSheet = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="业务员" |
| | | prop="salesperson" |
| | | required> |
| | | <up-input v-model="form.salesperson" |
| | | placeholder="请选择业务员" |
| | | readonly |
| | | @click="showSalespersonSheet = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showSalespersonSheet = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="报价日期" |
| | | prop="quotationDate" |
| | | required> |
| | | <up-input v-model="form.quotationDate" |
| | | placeholder="请选择报价日期" |
| | | readonly |
| | | @click="showQuotationDatePicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showQuotationDatePicker = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="有效期至" |
| | | prop="validDate" |
| | | required> |
| | | <up-input v-model="form.validDate" |
| | | placeholder="请选择有效期" |
| | | readonly |
| | | @click="showValidDatePicker = true" /> |
| | | <template #right> |
| | | <up-icon name="arrow-right" |
| | | @click="showValidDatePicker = true"></up-icon> |
| | | </template> |
| | | </up-form-item> |
| | | <up-form-item label="付款方式" |
| | | prop="paymentMethod" |
| | | required> |
| | | <up-input v-model="form.paymentMethod" |
| | | placeholder="请输入付款方式" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="备注" |
| | | prop="remark"> |
| | | <up-textarea v-model="form.remark" |
| | | placeholder="请输入备注" |
| | | auto-height /> |
| | | </up-form-item> |
| | | </u-cell-group> |
| | | <u-cell-group title="产品信息" |
| | | class="form-section"> |
| | | <view class="section-tools"> |
| | | <up-button type="primary" size="small" text="新增产品" :disabled="isEditMode" @click="addProduct" /> |
| | | <up-button type="primary" |
| | | size="small" |
| | | text="新增产品" |
| | | @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 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" |
| | | 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> |
| | | <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 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 label="规格型号"> |
| | | <up-input v-model="product.ProductModel" |
| | | 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.unit" |
| | | placeholder="请输入单位" |
| | | clearable /> |
| | | </up-form-item> |
| | | <up-form-item label="数量"> |
| | | <up-input v-model="product.quantity" type="number" placeholder="请输入数量" clearable @blur="calculateAmount(product)" /> |
| | | <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-input v-model="product.unitPrice" |
| | | type="number" |
| | | placeholder="请输入单价" |
| | | clearable |
| | | @blur="calculateAmount(product)" /> |
| | | </up-form-item> |
| | | <up-form-item label="金额"> |
| | | <up-input :model-value="formatAmount(product.amount)" disabled placeholder="自动计算(数量*单价)" /> |
| | | <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"> |
| | | <u-cell-group title="汇总信息" |
| | | class="form-section"> |
| | | <up-form-item label="报价总额"> |
| | | <up-input :model-value="formatAmount(totalAmount)" disabled placeholder="自动汇总" /> |
| | | <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" /> |
| | | <FooterButtons :loading="loading" |
| | | confirmText="保存" |
| | | @cancel="goBack" |
| | | @confirm="handleSubmit" /> |
| | | <up-action-sheet :show="showCustomerSheet" |
| | | title="选择客户" |
| | | :actions="customerActions" |
| | | @select="onSelectCustomer" |
| | | @close="showCustomerSheet = false" /> |
| | | <up-action-sheet :show="showSalespersonSheet" |
| | | title="选择业务员" |
| | | :actions="salespersonActions" |
| | | @select="onSelectSalesperson" |
| | | @close="showSalespersonSheet = false" /> |
| | | <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" /> |
| | | <up-datetime-picker :show="showQuotationDatePicker" |
| | | v-model="quotationDateValue" |
| | | mode="date" |
| | | @confirm="onQuotationDateConfirm" |
| | | @cancel="showQuotationDatePicker = false" /> |
| | | <up-datetime-picker :show="showValidDatePicker" |
| | | v-model="validDateValue" |
| | | mode="date" |
| | | @confirm="onValidDateConfirm" |
| | | @cancel="showValidDatePicker = false" /> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, onMounted, ref } from "vue"; |
| | | import { computed, onMounted, onUnmounted, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import FooterButtons from "@/components/FooterButtons.vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { formatDateToYMD } from "@/utils/ruoyi"; |
| | | import { modelList, productTreeList } from "@/api/basicData/product"; |
| | | import { addOrUpdateQuotationProduct, editQuotationProduct } from "@/api/salesManagement/salesQuotationProduct"; |
| | | import { userListNoPageByTenantId } from "@/api/system/user"; |
| | | import { |
| | | addQuotation, |
| | | getCustomerList, |
| | | updateQuotation, |
| | | } from "@/api/salesManagement/salesQuotation"; |
| | | |
| | | const formRef = ref(); |
| | | const loading = ref(false); |
| | | const quotationId = ref(""); |
| | | const showCustomerSheet = ref(false); |
| | | const showSalespersonSheet = ref(false); |
| | | const showProductSheet = ref(false); |
| | | const showModelSheet = ref(false); |
| | | const showQuotationDatePicker = ref(false); |
| | | const showValidDatePicker = ref(false); |
| | | const quotationDateValue = ref(Date.now()); |
| | | const validDateValue = ref(Date.now()); |
| | | const currentProductIndex = ref(-1); |
| | | const customerList = ref([]); |
| | | const salespersonList = ref([]); |
| | | const productList = ref([]); |
| | | const modelActions = ref([]); |
| | | |
| | | let uidSeed = 1; |
| | | |
| | | const form = ref({ |
| | | id: undefined, |
| | | quotationNo: "", |
| | | customerId: undefined, |
| | | customer: "", |
| | | salesperson: "", |
| | | quotationDate: "", |
| | | validDate: "", |
| | | paymentMethod: "", |
| | | status: "草稿", |
| | | remark: "", |
| | | products: [], |
| | | subtotal: 0, |
| | | freight: 0, |
| | | otherFee: 0, |
| | | discountRate: 0, |
| | | discountAmount: 0, |
| | | totalAmount: 0, |
| | | }); |
| | | |
| | | const rules = { |
| | | customer: [{ required: true, message: "请选择客户", trigger: "change" }], |
| | | salesperson: [{ required: true, message: "请选择业务员", trigger: "change" }], |
| | | quotationDate: [ |
| | | { required: true, message: "请选择报价日期", trigger: "change" }, |
| | | ], |
| | | validDate: [{ required: true, message: "请选择有效期", trigger: "change" }], |
| | | paymentMethod: [ |
| | | { required: true, message: "请输入付款方式", trigger: "blur" }, |
| | | ], |
| | | }; |
| | | |
| | | 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 totalAmount = computed(() => |
| | | Number( |
| | | (form.value.products || []) |
| | | .reduce((sum, item) => sum + Number(item.amount || 0), 0) |
| | | .toFixed(2) |
| | | ) |
| | | ); |
| | | const customerActions = computed(() => |
| | | customerList.value.map(item => ({ |
| | | name: item.customerName, |
| | | value: item.id, |
| | | })) |
| | | ); |
| | | const salespersonActions = computed(() => |
| | | salespersonList.value.map(item => ({ |
| | | name: item.nickName, |
| | | value: item.nickName, |
| | | })) |
| | | ); |
| | | const productActions = computed(() => |
| | | productList.value.map(item => ({ |
| | | name: item.label, |
| | | value: item.value, |
| | | label: item.label, |
| | | })) |
| | | ); |
| | | |
| | | const createEmptyProduct = () => ({ |
| | | uid: `p_${uidSeed++}`, |
| | | id: "", |
| | | salesQuotationId: "", |
| | | productId: "", |
| | | product: "", |
| | | specificationId: "", |
| | | specification: "", |
| | | productModelId: "", |
| | | ProductModel: "", |
| | | unit: "", |
| | | paper: "", |
| | | paperWeight: "", |
| | | quantity: 1, |
| | | unitPrice: 0, |
| | | printingFee: 0, |
| | | dieCuttingFee: 0, |
| | | grindingFee: 0, |
| | | amount: 0, |
| | | modelOptions: [], |
| | | }); |
| | |
| | | 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 }); |
| | | 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 calculateAmount = product => { |
| | | product.amount = Number( |
| | | (Number(product.quantity || 0) * Number(product.unitPrice || 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 addProduct = () => form.value.products.push(createEmptyProduct()); |
| | | const removeProduct = index => { |
| | | form.value.products.splice(index, 1); |
| | | syncTotalAmount(); |
| | | form.value.totalAmount = totalAmount.value; |
| | | }; |
| | | |
| | | const fetchModelOptions = async (productId, product) => { |
| | | const rows = await modelList({ id: productId }).catch(() => []); |
| | | product.modelOptions = Array.isArray(rows) ? rows : []; |
| | | try { |
| | | const res = await modelList({ id: productId }); |
| | | const rows = res?.data?.records || res?.data || res?.records || res || []; |
| | | product.modelOptions = Array.isArray(rows) ? rows : []; |
| | | } catch (error) { |
| | | console.error("获取规格型号失败:", error); |
| | | product.modelOptions = []; |
| | | } |
| | | }; |
| | | |
| | | const openProductPicker = index => { |
| | | currentProductIndex.value = index; |
| | | showProductSheet.value = true; |
| | | }; |
| | | |
| | | const openModelPicker = index => { |
| | | currentProductIndex.value = index; |
| | | const current = form.value.products[index]; |
| | |
| | | uni.showToast({ title: "请先选择产品", icon: "none" }); |
| | | return; |
| | | } |
| | | modelActions.value = (current.modelOptions || []).map(item => ({ name: item.model, value: item.id, unit: item.unit })); |
| | | modelActions.value = (current.modelOptions || []).map(item => ({ |
| | | name: item.model || item.specification, |
| | | value: item.id, |
| | | unit: item.unit, |
| | | })); |
| | | if (!modelActions.value.length) { |
| | | uni.showToast({ title: "暂无规格数据", icon: "none" }); |
| | | uni.showToast({ title: "暂无规格型号", icon: "none" }); |
| | | return; |
| | | } |
| | | showModelSheet.value = true; |
| | | }; |
| | | |
| | | const onSelectCustomer = action => { |
| | | form.value.customerId = action.value; |
| | | form.value.customer = action.name; |
| | | showCustomerSheet.value = false; |
| | | }; |
| | | const onSelectSalesperson = action => { |
| | | form.value.salesperson = action.value; |
| | | showSalespersonSheet.value = false; |
| | | }; |
| | | 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.productModelId = ""; |
| | | current.ProductModel = ""; |
| | | 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.productModelId = action.value; |
| | | current.ProductModel = action.name; |
| | | current.unit = action.unit || current.unit; |
| | | showModelSheet.value = false; |
| | | }; |
| | | const onQuotationDateConfirm = e => { |
| | | form.value.quotationDate = formatDateToYMD(e.value); |
| | | showQuotationDatePicker.value = false; |
| | | }; |
| | | const onValidDateConfirm = e => { |
| | | form.value.validDate = formatDateToYMD(e.value); |
| | | showValidDatePicker.value = false; |
| | | }; |
| | | |
| | | const fetchProductOptions = async () => { |
| | | const productTree = await productTreeList().catch(() => []); |
| | | productList.value = flattenProductTree(Array.isArray(productTree) ? productTree : productTree?.data || []); |
| | | const fetchBaseOptions = async () => { |
| | | const [customers, users, productTree] = await Promise.all([ |
| | | getCustomerList({ current: -1, size: -1 }).catch(() => ({})), |
| | | userListNoPageByTenantId().catch(() => ({})), |
| | | productTreeList().catch(() => []), |
| | | ]); |
| | | customerList.value = customers?.data?.records || customers?.records || []; |
| | | const userRows = users?.data || []; |
| | | salespersonList.value = Array.isArray(userRows) ? userRows : []; |
| | | productList.value = flattenProductTree( |
| | | Array.isArray(productTree) ? productTree : productTree?.data || [] |
| | | ); |
| | | }; |
| | | |
| | | // 根据名称反查节点 id,便于仅存名称时的反显 |
| | | const findNodeIdByLabel = (nodes, label) => { |
| | | if (!label) return null; |
| | | for (let i = 0; i < nodes.length; i++) { |
| | | const node = nodes[i]; |
| | | if (node.label === label) return node.value; |
| | | if (node.children && node.children.length > 0) { |
| | | const found = findNodeIdByLabel(node.children, label); |
| | | if (found !== null && found !== undefined) return found; |
| | | } |
| | | } |
| | | return null; |
| | | }; |
| | | |
| | | const normalizeProductRows = async rows => { |
| | | const normalized = await Promise.all( |
| | | (Array.isArray(rows) ? rows : []).map(async item => { |
| | | const productName = item.product || item.productName || ""; |
| | | // 优先用 productId;如果只有名称,尝试反查 id 以便选择器反显 |
| | | let resolvedProductId = |
| | | item.productId || |
| | | findNodeIdByLabel(productList.value, productName) || |
| | | ""; |
| | | |
| | | 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 || "", |
| | | productId: resolvedProductId, |
| | | product: productName, |
| | | productModelId: item.productModelId || "", |
| | | ProductModel: item.ProductModel || 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)), |
| | | amount: Number(item.amount || 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 || ""; |
| | | // 如果没有 productModelId 但有 ProductModel 名称,尝试从 modelOptions 中匹配 ID |
| | | if (!row.productModelId && row.ProductModel) { |
| | | const foundModel = row.modelOptions.find( |
| | | m => |
| | | m.model === row.ProductModel || |
| | | m.specification === row.ProductModel |
| | | ); |
| | | if (foundModel) { |
| | | row.productModelId = foundModel.id; |
| | | // 统一使用 modelOptions 中的字段 |
| | | row.ProductModel = |
| | | foundModel.model || foundModel.specification || row.ProductModel; |
| | | row.unit = foundModel.unit || row.unit; |
| | | } |
| | | } |
| | | } |
| | |
| | | form.value.products = normalized; |
| | | }; |
| | | |
| | | const loadEditFromStorage = async () => { |
| | | const loadDetail = 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 cachedData = uni.getStorageSync("salesQuotationDetail"); |
| | | if ( |
| | | cachedData && |
| | | (cachedData.id === quotationId.value || |
| | | cachedData.id === Number(quotationId.value)) |
| | | ) { |
| | | const data = cachedData; |
| | | form.value = { |
| | | ...form.value, |
| | | id: data.id, |
| | | quotationNo: data.quotationNo || "", |
| | | customerId: data.customerId, |
| | | customer: data.customer || "", |
| | | salesperson: data.salesperson || "", |
| | | quotationDate: data.quotationDate || "", |
| | | validDate: data.validDate || "", |
| | | paymentMethod: data.paymentMethod || "", |
| | | status: data.status || "草稿", |
| | | remark: data.remark || "", |
| | | subtotal: data.subtotal || 0, |
| | | freight: data.freight || 0, |
| | | otherFee: data.otherFee || 0, |
| | | discountRate: data.discountRate || 0, |
| | | discountAmount: data.discountAmount || 0, |
| | | totalAmount: data.totalAmount || 0, |
| | | }; |
| | | await normalizeProductRows(data.products || []); |
| | | form.value.totalAmount = totalAmount.value; |
| | | } else { |
| | | console.warn("未找到缓存的报价单详情数据"); |
| | | } |
| | | }; |
| | | |
| | | const validateProducts = () => { |
| | |
| | | uni.showToast({ title: "请至少添加一个产品", icon: "none" }); |
| | | return false; |
| | | } |
| | | const invalid = form.value.products.some(item => !item.productId || !item.specificationId || !item.unit || !Number(item.unitPrice || 0)); |
| | | const invalid = form.value.products.some( |
| | | item => |
| | | !item.productId || |
| | | !item.productModelId || |
| | | !item.unit || |
| | | !Number(item.quantity) || |
| | | !Number(item.unitPrice) |
| | | ); |
| | | 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; |
| | | |
| | | const valid = await formRef.value.validate().catch(() => false); |
| | | if (!valid || !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) |
| | | // 同步最新的总额 |
| | | form.value.totalAmount = totalAmount.value; |
| | | form.value.subtotal = totalAmount.value; |
| | | |
| | | const payload = { |
| | | ...form.value, |
| | | products: form.value.products.map(item => ({ |
| | | productId: item.productId, |
| | | product: item.product, |
| | | productModelId: item.productModelId, |
| | | ProductModel: item.ProductModel, |
| | | quantity: Number(item.quantity || 0), |
| | | unit: item.unit, |
| | | unitPrice: Number(item.unitPrice || 0), |
| | | amount: Number(item.amount || 0), |
| | | })), |
| | | }; |
| | | const action = quotationId.value ? updateQuotation : addQuotation; |
| | | action(payload) |
| | | .then(() => { |
| | | uni.showToast({ title: "保存成功", icon: "success" }); |
| | | setTimeout(() => uni.navigateBack(), 300); |
| | |
| | | quotationId.value = options.id; |
| | | form.value.id = options.id; |
| | | } else { |
| | | form.value.products = []; |
| | | const today = formatDateToYMD(Date.now()); |
| | | form.value.quotationDate = today; |
| | | form.value.validDate = today; |
| | | } |
| | | }); |
| | | |
| | | onMounted(async () => { |
| | | await fetchProductOptions(); |
| | | if (quotationId.value) await loadEditFromStorage(); |
| | | await fetchBaseOptions(); |
| | | if (quotationId.value) { |
| | | await loadDetail(); |
| | | } |
| | | }); |
| | | |
| | | onUnmounted(() => {}); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @import "@/static/scss/form-common.scss"; |
| | | |
| | | .account-detail { |
| | | min-height: 100vh; |
| | | background: #f8f9fa; |
| | | padding-bottom: 100px; |
| | | } |
| | | |
| | | .form-container { |
| | | padding: 12px 12px 0; |
| | | } |
| | | |
| | | .hero-card { |
| | | margin-bottom: 12px; |
| | | padding: 18px 18px 16px; |
| | | border-radius: 16px; |
| | | background: linear-gradient(135deg, #eef6ff 0%, #ffffff 100%); |
| | | box-shadow: 0 6px 18px rgba(41, 121, 255, 0.08); |
| | | } |
| | | |
| | | .hero-title { |
| | | display: block; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1f2d3d; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .hero-desc { |
| | | display: block; |
| | | font-size: 13px; |
| | | line-height: 1.6; |
| | | color: #7a8599; |
| | | } |
| | | |
| | | .form-section { |
| | |
| | | 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) { |