优化合同个管理和产品管理相关功能

This commit is contained in:
2026-09-12 23:29:45 +08:00
parent 2b1874943f
commit 4455b95e37
92 changed files with 5367 additions and 3144 deletions
@@ -19,6 +19,21 @@
<el-input v-model="form.product_no" placeholder="如:P-2026-001" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="行类型" prop="line_type">
<el-select v-model="form.line_type" placeholder="请选择" style="width: 100%" @change="onLineTypeChange">
<el-option label="成品" value="product" />
<el-option label="软件开发" value="dev" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.line_type === 'dev'">
<el-form-item label="统一人天单价" prop="dev_unit_price">
<el-input-number v-model="form.dev_unit_price" :min="0" :precision="2" :controls="false" style="width: 100%">
<template #prefix>¥</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品分类" prop="category">
<el-select
@@ -45,21 +60,29 @@
<el-input v-model="form.spec" placeholder="如:型号 / 配置 / 功能模块" maxlength="255" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="24" v-if="form.line_type === 'dev'">
<el-form-item label="模块报价" prop="dev_tree">
<el-button type="primary" link @click="devVisible = true">
📁 编辑模块树({{ (form.dev_tree || []).length }} 项 · ¥{{ formatMoney(treeSellTotal(form.dev_tree, form.dev_unit_price)) }})
</el-button>
<div class="dev-tip">叶子模块只填「人天」,人天单价统一;父级自动汇总金额</div>
</el-form-item>
</el-col>
<el-col :span="8" v-if="form.line_type === 'product'">
<el-form-item label="销售单价" prop="price">
<el-input-number v-model="form.price" :min="0" :precision="2" :controls="false" style="width: 100%">
<template #prefix>¥</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" v-if="form.line_type === 'product'">
<el-form-item label="成本单价" prop="cost_price">
<el-input-number v-model="form.cost_price" :min="0" :precision="2" :controls="false" style="width: 100%">
<template #prefix>¥</template>
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="8">
<el-col :span="8" v-if="form.line_type === 'product'">
<el-form-item label="税率(%)" prop="tax_rate">
<el-input-number v-model="form.tax_rate" :min="0" :max="100" :precision="2" :controls="false" style="width: 100%" />
</el-form-item>
@@ -78,6 +101,15 @@
</el-col>
</el-row>
</el-form>
<DevModuleTree
v-model="form.dev_tree"
:unit-price="form.dev_unit_price"
v-model:visible="devVisible"
@update:unit-price="(v) => (form.dev_unit_price = v)"
:title="form.product_name ? form.product_name + ' - 模块报价' : '软件开发模块报价'"
/>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
@@ -90,7 +122,9 @@ import { ref, reactive, watch } from "vue";
import { ElMessage } from "element-plus";
import { createProduct, updateProduct } from "@/api/crmProduct";
import { getProductCategoryList } from "@/api/crmProductCategory";
import { PRODUCT_STATUS_OPTIONS, PRODUCT_UNIT_OPTIONS } from "../../dict";
import { PRODUCT_STATUS_OPTIONS, PRODUCT_UNIT_OPTIONS, formatMoney } from "../../dict";
import { treeSellTotal } from "../../contract/components/utils";
import DevModuleTree from "../../contract/components/DevModuleTree.vue";
const props = defineProps({
visible: { type: Boolean, default: false },
@@ -103,6 +137,7 @@ const submitting = ref(false);
const isEdit = ref(false);
const internalId = ref(null);
const categoryOptions = ref([]);
const devVisible = ref(false);
const defaultForm = () => ({
product_name: "",
@@ -110,9 +145,12 @@ const defaultForm = () => ({
category: "",
unit: "",
spec: "",
line_type: "product",
price: 0,
cost_price: 0,
tax_rate: 0,
dev_unit_price: 0,
dev_tree: [],
status: 1,
remark: "",
});
@@ -123,6 +161,28 @@ const rules = {
product_name: [{ required: true, message: "请输入产品名称", trigger: "blur" }],
};
/** 切换行类型:软件开发确保有模块树容器 */
function onLineTypeChange() {
if (form.line_type === "dev") {
if (!Array.isArray(form.dev_tree)) form.dev_tree = [];
if (form.dev_unit_price == null) form.dev_unit_price = 0;
}
}
/** 安全解析产品管理返回的 dev_tree(可能为字符串或数组) */
function normalizeDevTree(v) {
if (Array.isArray(v)) return v;
if (typeof v === "string" && v) {
try {
const parsed = JSON.parse(v);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
return [];
}
}
return [];
}
watch(
() => props.visible,
(val) => {
@@ -133,6 +193,9 @@ watch(
Object.assign(form, defaultForm(), props.editData);
form.status = Number(props.editData.status);
if (isNaN(form.status)) form.status = 1;
form.line_type = props.editData.line_type === "dev" ? "dev" : "product";
form.dev_unit_price = Number(props.editData.dev_unit_price) || 0;
form.dev_tree = normalizeDevTree(props.editData.dev_tree);
} else {
isEdit.value = false;
internalId.value = null;