<template>
|
<view class="account-detail">
|
<PageHeader :title="pageTitle"
|
@back="goBack" />
|
<view class="form-container">
|
<up-form ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-width="120"
|
input-align="right"
|
error-message-align="right">
|
<u-cell-group title="合同主档"
|
class="form-section">
|
<up-form-item label="合同编号"
|
prop="contractNo"
|
required>
|
<up-input v-model="form.contractNo"
|
placeholder="请输入合同编号"
|
clearable />
|
</up-form-item>
|
<up-form-item label="签订时间"
|
prop="signDate"
|
required>
|
<FormPicker v-model="form.signDate"
|
type="date"
|
placeholder="请选择签订时间" />
|
</up-form-item>
|
<up-form-item label="签约单位简称"
|
prop="signingUnit"
|
required>
|
<up-input v-model="form.signingUnit"
|
placeholder="请输入签约单位简称"
|
clearable />
|
</up-form-item>
|
<up-form-item label="签约周期"
|
prop="contractPeriod">
|
<up-input v-model="form.contractPeriod"
|
placeholder="请输入签约周期"
|
clearable />
|
</up-form-item>
|
<up-form-item label="存档状态"
|
prop="archiveStatus"
|
required>
|
<FormPicker v-model="form.archiveStatus"
|
type="select"
|
title="选择存档状态"
|
:options="ARCHIVE_STATUS_OPTIONS"
|
placeholder="请选择存档状态" />
|
</up-form-item>
|
<up-form-item label="存档情况说明"
|
prop="archiveRemark">
|
<up-input v-model="form.archiveRemark"
|
placeholder="请输入存档情况说明"
|
clearable />
|
</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="productList.length === 0"
|
class="empty-text">
|
<text>暂无产品,请先添加产品</text>
|
</view>
|
<view v-else
|
class="product-list">
|
<view v-for="(row, index) in productList"
|
:key="row.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="row.productCategory"
|
placeholder="请选择产品大类"
|
readonly
|
@click="openCategoryPicker(index)" />
|
<template #right>
|
<up-icon name="arrow-right"
|
@click="openCategoryPicker(index)"></up-icon>
|
</template>
|
</up-form-item>
|
<up-form-item label="规格型号">
|
<up-input v-model="row.specificationModel"
|
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="row.contractUnit"
|
placeholder="请输入计量单位"
|
clearable />
|
</up-form-item>
|
<up-form-item label="含税单价">
|
<up-input v-model="row.taxInclusiveUnitPrice"
|
type="number"
|
placeholder="请输入含税单价"
|
clearable />
|
</up-form-item>
|
<up-form-item label="约定/暂估用量">
|
<up-input v-model="row.agreedQuantity"
|
type="number"
|
placeholder="请输入约定用量"
|
clearable />
|
</up-form-item>
|
<up-form-item label="已发货数量">
|
<up-input v-model="row.shippedQuantity"
|
type="number"
|
placeholder="请输入已发货数量"
|
clearable />
|
</up-form-item>
|
<up-form-item label="产值">
|
<up-input :model-value="formatAmount(productOutputValue(row))"
|
disabled
|
placeholder="自动计算" />
|
</up-form-item>
|
</view>
|
</view>
|
</view>
|
</u-cell-group>
|
|
<u-cell-group title="履约与质保"
|
class="form-section">
|
<up-form-item label="履约保证金">
|
<up-input v-model="form.performanceBond"
|
type="number"
|
placeholder="请输入履约保证金"
|
clearable />
|
</up-form-item>
|
<up-form-item label="质保金情况">
|
<up-input v-model="form.warrantyDeposit"
|
type="number"
|
placeholder="请输入质保金"
|
clearable />
|
</up-form-item>
|
<up-form-item label="产值">
|
<up-input :model-value="formatAmount(computedOutputValue)"
|
disabled
|
placeholder="自动汇总" />
|
</up-form-item>
|
</u-cell-group>
|
|
<u-cell-group title="回款与结算"
|
class="form-section">
|
<up-form-item label="已回款金额">
|
<up-input v-model="form.receivedAmount"
|
type="number"
|
placeholder="请输入已回款金额"
|
clearable />
|
</up-form-item>
|
<up-form-item label="结算次数">
|
<up-input v-model="form.settlementCount"
|
type="number"
|
placeholder="请输入结算次数"
|
clearable />
|
</up-form-item>
|
<up-form-item label="应收金额">
|
<up-input v-model="form.receivableAmount"
|
type="number"
|
placeholder="请输入应收金额"
|
clearable />
|
</up-form-item>
|
<up-form-item label="应付比例">
|
<up-input v-model="form.payableRatio"
|
placeholder="例如:30% 或 0.3"
|
clearable />
|
</up-form-item>
|
<up-form-item label="合同节点应收">
|
<up-input v-model="form.nodeReceivable"
|
type="number"
|
placeholder="请输入合同节点应收"
|
clearable />
|
</up-form-item>
|
<up-form-item label="约定付款节点">
|
<up-input v-model="form.paymentNodes"
|
placeholder="请输入合同约定付款节点"
|
clearable />
|
</up-form-item>
|
<up-form-item label="结算记录">
|
<up-textarea v-model="form.settlementRecord"
|
placeholder="请输入结算记录"
|
auto-height />
|
</up-form-item>
|
</u-cell-group>
|
|
<u-cell-group title="开票与违约"
|
class="form-section">
|
<up-form-item label="开票要求">
|
<up-input v-model="form.invoiceRequirement"
|
placeholder="请输入开票要求"
|
clearable />
|
</up-form-item>
|
<up-form-item label="最近开票时间">
|
<FormPicker v-model="form.lastInvoiceDate"
|
type="date"
|
placeholder="请选择最近一次开票时间" />
|
</up-form-item>
|
<up-form-item label="已开票金额">
|
<up-input v-model="form.invoicedAmount"
|
type="number"
|
placeholder="请输入已开票金额"
|
clearable />
|
</up-form-item>
|
<up-form-item label="违约金约定">
|
<up-textarea v-model="form.penaltyAgreement"
|
placeholder="请输入违约金约定"
|
auto-height />
|
</up-form-item>
|
<up-form-item label="情况说明">
|
<up-textarea v-model="form.remark"
|
placeholder="请输入情况说明"
|
auto-height />
|
</up-form-item>
|
</u-cell-group>
|
</up-form>
|
</view>
|
|
<FooterButtons :loading="loading"
|
confirmText="保存"
|
@cancel="goBack"
|
@confirm="handleSubmit" />
|
|
<up-action-sheet :show="showCategorySheet"
|
title="选择产品大类"
|
:actions="categoryActions"
|
@select="onSelectCategory"
|
@close="showCategorySheet = 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 FormPicker from "@/components/FormPicker.vue";
|
import { modelList, productTreeList } from "@/api/basicData/product";
|
import { loadDetailData } from "@/utils/detailCache";
|
import {
|
addOrUpdateContract,
|
getContractDetail,
|
} from "@/api/salesManagement/contractManagement";
|
|
const ARCHIVE_STATUS_OPTIONS = [
|
{ name: "已存档", value: "已存档" },
|
{ name: "已用章未寄回", value: "已用章未寄回" },
|
{ name: "未签约", value: "未签约" },
|
];
|
|
const formRef = ref();
|
const loading = ref(false);
|
const contractId = ref("");
|
const productList = ref([]);
|
const categoryOptions = ref([]);
|
const categoryActions = ref([]);
|
const modelActions = ref([]);
|
const showCategorySheet = ref(false);
|
const showModelSheet = ref(false);
|
const currentProductIndex = ref(-1);
|
|
let uidSeed = 1;
|
|
const form = ref({
|
id: undefined,
|
contractNo: "",
|
signDate: "",
|
signingUnit: "",
|
contractPeriod: "",
|
archiveStatus: "",
|
archiveRemark: "",
|
performanceBond: undefined,
|
warrantyDeposit: undefined,
|
outputValue: undefined,
|
receivedAmount: undefined,
|
settlementCount: undefined,
|
receivableAmount: undefined,
|
payableRatio: "",
|
nodeReceivable: undefined,
|
paymentNodes: "",
|
settlementRecord: "",
|
invoiceRequirement: "",
|
lastInvoiceDate: "",
|
invoicedAmount: undefined,
|
penaltyAgreement: "",
|
remark: "",
|
});
|
|
const rules = {
|
contractNo: [{ required: true, message: "请输入合同编号", trigger: "blur" }],
|
signDate: [{ required: true, message: "请选择签订时间", trigger: "change" }],
|
signingUnit: [
|
{ required: true, message: "请输入签约单位简称", trigger: "blur" },
|
],
|
archiveStatus: [
|
{ required: true, message: "请选择存档状态", trigger: "change" },
|
],
|
};
|
|
const pageTitle = computed(() => (contractId.value ? "编辑合同" : "新增合同"));
|
|
const productOutputValue = row =>
|
Number(
|
(
|
Number(row?.taxInclusiveUnitPrice || 0) *
|
Number(row?.shippedQuantity || 0)
|
).toFixed(2)
|
);
|
|
const computedOutputValue = computed(() =>
|
Number(
|
productList.value
|
.reduce((sum, row) => sum + productOutputValue(row), 0)
|
.toFixed(2)
|
)
|
);
|
|
const formatAmount = amount => `¥${Number(amount || 0).toFixed(2)}`;
|
const goBack = () => uni.navigateBack();
|
|
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 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 createEmptyProduct = () => ({
|
uid: `p_${uidSeed++}`,
|
categoryId: "",
|
productCategory: "",
|
productModelId: "",
|
specificationModel: "",
|
contractUnit: "",
|
taxInclusiveUnitPrice: undefined,
|
agreedQuantity: undefined,
|
shippedQuantity: undefined,
|
modelOptions: [],
|
});
|
|
const addProduct = () => productList.value.push(createEmptyProduct());
|
|
const removeProduct = index => {
|
productList.value.splice(index, 1);
|
};
|
|
const fetchBaseOptions = async () => {
|
try {
|
const res = await productTreeList();
|
const nodes = Array.isArray(res) ? res : res?.data || [];
|
categoryOptions.value = flattenProductTree(nodes);
|
} catch (error) {
|
categoryOptions.value = [];
|
}
|
};
|
|
const fetchModelOptions = async (categoryId, row) => {
|
try {
|
const res = await modelList({ id: categoryId });
|
const rows = res?.data?.records || res?.data || res?.records || res || [];
|
row.modelOptions = Array.isArray(rows) ? rows : [];
|
} catch (error) {
|
row.modelOptions = [];
|
}
|
};
|
|
const openCategoryPicker = index => {
|
currentProductIndex.value = index;
|
categoryActions.value = categoryOptions.value.map(item => ({
|
name: item.label,
|
value: item.value,
|
label: item.label,
|
}));
|
showCategorySheet.value = true;
|
};
|
|
const openModelPicker = index => {
|
currentProductIndex.value = index;
|
const row = productList.value[index];
|
if (!row?.categoryId) {
|
uni.showToast({ title: "请先选择产品大类", icon: "none" });
|
return;
|
}
|
modelActions.value = (row.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 onSelectCategory = action => {
|
const row = productList.value[currentProductIndex.value];
|
showCategorySheet.value = false;
|
if (!row) return;
|
row.categoryId = action.value;
|
row.productCategory = action.label;
|
row.productModelId = "";
|
row.specificationModel = "";
|
row.contractUnit = "";
|
row.modelOptions = [];
|
fetchModelOptions(action.value, row);
|
};
|
|
const onSelectModel = action => {
|
const row = productList.value[currentProductIndex.value];
|
showModelSheet.value = false;
|
if (!row) return;
|
row.productModelId = action.value;
|
row.specificationModel = action.name;
|
row.contractUnit = action.unit || row.contractUnit;
|
};
|
|
const normalizeProductRows = async rows => {
|
const normalized = await Promise.all(
|
(Array.isArray(rows) ? rows : []).map(async item => {
|
const row = {
|
uid: `p_${uidSeed++}`,
|
categoryId:
|
item.categoryId ||
|
findNodeIdByLabel(categoryOptions.value, item.productCategory) ||
|
"",
|
productCategory: item.productCategory || "",
|
productModelId: item.productModelId || "",
|
specificationModel: item.specificationModel || "",
|
contractUnit: item.contractUnit || "",
|
taxInclusiveUnitPrice: item.taxInclusiveUnitPrice,
|
agreedQuantity: item.agreedQuantity,
|
shippedQuantity: item.shippedQuantity,
|
modelOptions: [],
|
};
|
if (row.categoryId) {
|
await fetchModelOptions(row.categoryId, row);
|
// 仅存了型号名称时,从型号列表反查 id,保证选择器能反显
|
if (!row.productModelId && row.specificationModel) {
|
const matched = row.modelOptions.find(
|
m => (m.model || m.specification) === row.specificationModel
|
);
|
if (matched) row.productModelId = matched.id;
|
}
|
}
|
return row;
|
})
|
);
|
productList.value = normalized;
|
};
|
|
const loadDetail = async () => {
|
if (!contractId.value) return;
|
let data = null;
|
try {
|
data = await loadDetailData({
|
cacheKey: "contractDetail",
|
id: contractId.value,
|
fetcher: getContractDetail,
|
});
|
} catch (error) {
|
data = null;
|
}
|
if (!data) {
|
uni.showToast({ title: "获取合同详情失败", icon: "none" });
|
return;
|
}
|
const { productList: rows, ...rest } = data;
|
form.value = { ...form.value, ...rest, id: data.id };
|
await normalizeProductRows(rows);
|
};
|
|
const buildPayload = () => ({
|
...form.value,
|
outputValue: computedOutputValue.value,
|
productList: productList.value.map(row => ({
|
productCategory: row.productCategory,
|
specificationModel: row.specificationModel,
|
productModelId: row.productModelId,
|
contractUnit: row.contractUnit,
|
taxInclusiveUnitPrice: Number(row.taxInclusiveUnitPrice || 0),
|
agreedQuantity: Number(row.agreedQuantity || 0),
|
shippedQuantity: Number(row.shippedQuantity || 0),
|
outputValue: productOutputValue(row),
|
})),
|
});
|
|
const handleSubmit = async () => {
|
const valid = await formRef.value.validate().catch(() => false);
|
if (!valid) return;
|
|
loading.value = true;
|
addOrUpdateContract(buildPayload())
|
.then(() => {
|
uni.showToast({ title: "保存成功", icon: "success" });
|
setTimeout(() => uni.navigateBack(), 300);
|
})
|
.catch(() => {
|
uni.showToast({ title: "保存失败", icon: "none" });
|
})
|
.finally(() => {
|
loading.value = false;
|
});
|
};
|
|
onLoad(options => {
|
if (options?.id) {
|
contractId.value = options.id;
|
form.value.id = options.id;
|
}
|
});
|
|
onMounted(async () => {
|
await fetchBaseOptions();
|
if (contractId.value) await loadDetail();
|
});
|
</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;
|
}
|
|
.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-card {
|
background: #fff;
|
border-radius: 12px;
|
padding: 0 12px 12px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
}
|
|
.product-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 12px 0;
|
}
|
|
.product-title {
|
font-size: 14px;
|
font-weight: 600;
|
color: #22324d;
|
}
|
|
.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>
|