<template>
|
<view class="account-detail">
|
<PageHeader :title="pageTitle"
|
@back="goBack" />
|
<view class="form-container">
|
<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="新增产品"
|
@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"
|
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.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.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 :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-input :model-value="formatAmount(totalAmount)"
|
disabled
|
placeholder="自动汇总" />
|
</up-form-item>
|
</u-cell-group>
|
</up-form>
|
</view>
|
<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, 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 { 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 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++}`,
|
productId: "",
|
product: "",
|
productModelId: "",
|
ProductModel: "",
|
unit: "",
|
quantity: 1,
|
unitPrice: 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 formatAmount = amount => `¥${Number(amount || 0).toFixed(2)}`;
|
const goBack = () => uni.navigateBack();
|
|
const calculateAmount = product => {
|
product.amount = Number(
|
(Number(product.quantity || 0) * Number(product.unitPrice || 0)).toFixed(2)
|
);
|
form.value.totalAmount = totalAmount.value;
|
};
|
|
const addProduct = () => form.value.products.push(createEmptyProduct());
|
const removeProduct = index => {
|
form.value.products.splice(index, 1);
|
form.value.totalAmount = totalAmount.value;
|
};
|
|
const fetchModelOptions = async (productId, product) => {
|
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];
|
if (!current?.productId) {
|
uni.showToast({ title: "请先选择产品", icon: "none" });
|
return;
|
}
|
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" });
|
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.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.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 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++}`,
|
productId: resolvedProductId,
|
product: productName,
|
productModelId: item.productModelId || "",
|
ProductModel: item.ProductModel || item.specification || "",
|
unit: item.unit || "",
|
quantity: Number(item.quantity || 1),
|
unitPrice: Number(item.unitPrice || 0),
|
amount: Number(item.amount || 0),
|
modelOptions: [],
|
};
|
|
if (row.productId) {
|
await fetchModelOptions(row.productId, row);
|
// 如果没有 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;
|
}
|
}
|
}
|
return row;
|
})
|
);
|
form.value.products = normalized;
|
};
|
|
const loadDetail = async () => {
|
if (!quotationId.value) return;
|
|
// 直接从本地存储获取数据,不再调用详情接口
|
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 = () => {
|
if (!form.value.products.length) {
|
uni.showToast({ title: "请至少添加一个产品", icon: "none" });
|
return false;
|
}
|
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 handleSubmit = async () => {
|
const valid = await formRef.value.validate().catch(() => false);
|
if (!valid || !validateProducts()) return;
|
loading.value = true;
|
|
// 同步最新的总额
|
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);
|
})
|
.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 {
|
const today = formatDateToYMD(Date.now());
|
form.value.quotationDate = today;
|
form.value.validDate = today;
|
}
|
});
|
|
onMounted(async () => {
|
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 {
|
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;
|
}
|
|
: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>
|