批量优化产品和产品分类

This commit is contained in:
2026-09-12 00:44:13 +08:00
parent 184c604bee
commit 2b1874943f
33 changed files with 2595 additions and 59 deletions
@@ -0,0 +1,94 @@
<template>
<el-drawer
:model-value="visible"
:title="product?.product_name || '产品详情'"
size="520px"
direction="rtl"
@update:model-value="handleClose"
>
<div v-if="product" class="product-detail">
<el-descriptions :column="1" border>
<el-descriptions-item label="产品编号">{{ product.product_no || "-" }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ product.product_name }}</el-descriptions-item>
<el-descriptions-item label="产品分类">
<el-tag :type="categoryType(product.category)" size="small" effect="plain">
{{ categoryDisplay(product.category) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="规格型号">{{ product.spec || "-" }}</el-descriptions-item>
<el-descriptions-item label="单位">{{ product.unit || "-" }}</el-descriptions-item>
<el-descriptions-item label="销售单价">¥{{ formatMoney(product.price) }}</el-descriptions-item>
<el-descriptions-item label="成本单价">¥{{ formatMoney(product.cost_price) }}</el-descriptions-item>
<el-descriptions-item label="税率">{{ formatPercent(product.tax_rate) }}</el-descriptions-item>
<el-descriptions-item label="毛利">
¥{{ formatMoney((Number(product.price) || 0) - (Number(product.cost_price) || 0)) }}
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="productStatusTag(product.status)" size="small">{{ productStatusText(product.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="来源项目">{{ product.project_name || "手动创建" }}</el-descriptions-item>
<el-descriptions-item label="备注">{{ product.remark || "-" }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDateTime(product.create_time) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDateTime(product.update_time) }}</el-descriptions-item>
</el-descriptions>
</div>
</el-drawer>
</template>
<script setup>
import { reactive, watch } from "vue";
import {
productCategoryText,
productCategoryTag,
productStatusText,
productStatusTag,
formatMoney,
formatPercent,
formatDateTime,
} from "../../dict";
import { getProductCategoryList } from "@/api/crmProductCategory";
const props = defineProps({
visible: { type: Boolean, default: false },
product: { type: Object, default: null },
});
const emit = defineEmits(["update:visible"]);
const categoryMap = reactive({});
/** 详情分类展示:优先管理分类名称;同步写入的产品带「金额归属」编码时回退 */
function categoryDisplay(val) {
if (!val) return "-";
if (categoryMap[val]) return val;
return productCategoryText(val);
}
function categoryType(val) {
if (!val || categoryMap[val]) return "primary";
return productCategoryTag(val);
}
watch(
() => props.visible,
(val) => {
if (val && Object.keys(categoryMap).length === 0) {
getProductCategoryList({ page: 1, pageSize: 200 })
.then((res) => {
(res?.data?.list || []).forEach((c) => {
categoryMap[c.name] = c.name;
});
})
.catch(() => {});
}
}
);
function handleClose() {
emit("update:visible", false);
}
</script>
<style lang="less" scoped>
.product-detail {
padding: 4px;
}
</style>
@@ -0,0 +1,189 @@
<template>
<el-dialog
:model-value="visible"
:title="isEdit ? '编辑产品' : '新增产品'"
width="720px"
:close-on-click-modal="false"
@update:model-value="handleClose"
@opened="handleOpened"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" label-position="right">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产品名称" prop="product_name">
<el-input v-model="form.product_name" placeholder="请输入产品名称" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="产品编号" prop="product_no">
<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="category">
<el-select
v-model="form.category"
filterable
allow-create
clearable
placeholder="请选择或输入分类"
style="width: 100%"
>
<el-option v-for="i in categoryOptions" :key="i.id" :label="i.name" :value="i.name" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="单位" prop="unit">
<el-select v-model="form.unit" filterable allow-create clearable placeholder="请选择" style="width: 100%">
<el-option v-for="i in PRODUCT_UNIT_OPTIONS" :key="i.value" :label="i.label" :value="i.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="规格型号" prop="spec">
<el-input v-model="form.spec" placeholder="如:型号 / 配置 / 功能模块" maxlength="255" />
</el-form-item>
</el-col>
<el-col :span="8">
<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-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-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>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择" style="width: 100%">
<el-option v-for="i in PRODUCT_STATUS_OPTIONS" :key="i.value" :label="i.label" :value="Number(i.value)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注(选填)" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</template>
</el-dialog>
</template>
<script setup>
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";
const props = defineProps({
visible: { type: Boolean, default: false },
editData: { type: Object, default: null },
});
const emit = defineEmits(["update:visible", "success"]);
const formRef = ref();
const submitting = ref(false);
const isEdit = ref(false);
const internalId = ref(null);
const categoryOptions = ref([]);
const defaultForm = () => ({
product_name: "",
product_no: "",
category: "",
unit: "",
spec: "",
price: 0,
cost_price: 0,
tax_rate: 0,
status: 1,
remark: "",
});
const form = reactive(defaultForm());
const rules = {
product_name: [{ required: true, message: "请输入产品名称", trigger: "blur" }],
};
watch(
() => props.visible,
(val) => {
if (val) {
if (props.editData) {
isEdit.value = true;
internalId.value = props.editData.id;
Object.assign(form, defaultForm(), props.editData);
form.status = Number(props.editData.status);
if (isNaN(form.status)) form.status = 1;
} else {
isEdit.value = false;
internalId.value = null;
Object.assign(form, defaultForm());
}
fetchCategories();
}
}
);
/** 加载产品分类(用于分类下拉;取启用中的分类,允许新建时输入自定义分类) */
async function fetchCategories() {
try {
const res = await getProductCategoryList({ page: 1, pageSize: 200, status: 1 });
categoryOptions.value = res?.data?.list || [];
} catch (e) {
categoryOptions.value = [];
}
}
function handleClose() {
emit("update:visible", false);
}
function handleOpened() {
formRef.value?.clearValidate();
}
async function handleSubmit() {
if (!formRef.value) return;
try {
await formRef.value.validate();
} catch (e) {
return;
}
submitting.value = true;
try {
const payload = { ...form };
if (internalId.value) {
await updateProduct(internalId.value, payload);
ElMessage.success("更新成功");
} else {
await createProduct(payload);
ElMessage.success("创建成功");
}
emit("success");
handleClose();
} catch (e) {
ElMessage.error(e.message || "操作失败");
} finally {
submitting.value = false;
}
}
</script>