<!--
|
项目产品信息子表的一行
|
|
row 由父级 productList 的元素直接传入(同一个响应式对象),
|
组件内直接改 row.xxx 即可,不需要 emit 回父级合并。
|
产品大类 / 规格型号走 up-action-sheet,选项由本组件自己拉取。
|
-->
|
<template>
|
<view class="product-card">
|
<view class="product-header">
|
<text class="product-title">产品 {{ index + 1 }}</text>
|
<up-icon name="trash"
|
color="#ee0a24"
|
size="18"
|
@click="$emit('remove')"></up-icon>
|
</view>
|
<up-divider></up-divider>
|
|
<view class="product-body">
|
<view class="field-row"
|
@click="openCategory">
|
<text class="field-label">产品大类</text>
|
<text :class="['field-value', row.productCategory ? '' : 'placeholder']">
|
{{ row.productCategory || "请选择产品大类" }}
|
</text>
|
<up-icon name="arrow-right"
|
size="14"
|
color="#c0c4cc"></up-icon>
|
</view>
|
|
<view class="field-row"
|
@click="openModel">
|
<text class="field-label">规格型号</text>
|
<text :class="['field-value', row.specificationModel ? '' : 'placeholder']">
|
{{ row.specificationModel || "请选择规格型号" }}
|
</text>
|
<up-icon name="arrow-right"
|
size="14"
|
color="#c0c4cc"></up-icon>
|
</view>
|
|
<view class="field-row">
|
<text class="field-label">单位</text>
|
<u-input v-model="row.unit"
|
placeholder="请输入单位"
|
border="none"
|
inputAlign="right" />
|
</view>
|
|
<view class="field-row">
|
<text class="field-label">数量</text>
|
<u-input v-model="row.quantity"
|
type="number"
|
placeholder="请输入数量"
|
border="none"
|
inputAlign="right" />
|
</view>
|
|
<view class="field-row"
|
@click="openTaxRate">
|
<text class="field-label">税率(%)</text>
|
<text :class="['field-value', row.taxRate ? '' : 'placeholder']">
|
{{ taxRateText || "请选择税率" }}
|
</text>
|
<up-icon name="arrow-right"
|
size="14"
|
color="#c0c4cc"></up-icon>
|
</view>
|
|
<view class="field-row">
|
<text class="field-label">含税单价</text>
|
<u-input v-model="row.taxInclusiveUnitPrice"
|
type="number"
|
placeholder="请输入含税单价"
|
border="none"
|
inputAlign="right" />
|
</view>
|
|
<view class="field-row">
|
<text class="field-label">含税总价</text>
|
<text class="field-value readonly">{{ formatAmount(inclusiveTotal) }}</text>
|
</view>
|
|
<view class="field-row">
|
<text class="field-label">不含税总价</text>
|
<u-input v-model="row.taxExclusiveTotalPrice"
|
type="number"
|
placeholder="请输入不含税总价"
|
border="none"
|
inputAlign="right" />
|
</view>
|
|
<view class="field-row"
|
@click="openInvoiceType">
|
<text class="field-label">发票类型</text>
|
<text :class="['field-value', row.invoiceType ? '' : 'placeholder']">
|
{{ row.invoiceType || "请选择发票类型" }}
|
</text>
|
<up-icon name="arrow-right"
|
size="14"
|
color="#c0c4cc"></up-icon>
|
</view>
|
</view>
|
|
<up-action-sheet :show="showCategorySheet"
|
title="选择产品大类"
|
:actions="categoryActions"
|
@select="onSelectCategory"
|
@close="showCategorySheet = false"></up-action-sheet>
|
|
<up-action-sheet :show="showModelSheet"
|
title="选择规格型号"
|
:actions="modelActions"
|
@select="onSelectModel"
|
@close="showModelSheet = false"></up-action-sheet>
|
|
<up-action-sheet :show="showTaxRateSheet"
|
title="选择税率"
|
:actions="taxRateActions"
|
@select="onSelectTaxRate"
|
@close="showTaxRateSheet = false"></up-action-sheet>
|
|
<up-action-sheet :show="showInvoiceSheet"
|
title="选择发票类型"
|
:actions="INVOICE_TYPE_ACTIONS"
|
@select="onSelectInvoiceType"
|
@close="showInvoiceSheet = false"></up-action-sheet>
|
</view>
|
</template>
|
|
<script setup>
|
import { computed, onMounted, ref } from "vue";
|
import { modelList, productTreeList } from "@/api/basicData/product";
|
import { productInclusiveTotal } from "../_utils/mapper";
|
|
const INVOICE_TYPE_ACTIONS = [
|
{ name: "增普票", value: "增普票" },
|
{ name: "增专票", value: "增专票" },
|
];
|
|
const props = defineProps({
|
row: {
|
type: Object,
|
required: true,
|
},
|
index: {
|
type: Number,
|
default: 0,
|
},
|
// 税率字典(useDict('tax_rate') 的返回值)
|
taxRateOptions: {
|
type: Array,
|
default: () => [],
|
},
|
});
|
|
defineEmits(["remove"]);
|
|
const categoryActions = ref([]);
|
const modelActions = ref([]);
|
const showCategorySheet = ref(false);
|
const showModelSheet = ref(false);
|
const showTaxRateSheet = ref(false);
|
const showInvoiceSheet = ref(false);
|
|
const inclusiveTotal = computed(() => productInclusiveTotal(props.row));
|
|
const taxRateActions = computed(() =>
|
(props.taxRateOptions || []).map(item => ({
|
name: item.label,
|
value: item.value,
|
}))
|
);
|
|
const taxRateText = computed(() => {
|
const matched = (props.taxRateOptions || []).find(
|
item => String(item.value) === String(props.row.taxRate)
|
);
|
return matched ? matched.label : "";
|
});
|
|
const formatAmount = value => Number(value || 0).toFixed(2);
|
|
const flattenTree = nodes => {
|
const result = [];
|
const walk = list => {
|
(list || []).forEach(item => {
|
if (item.children && item.children.length) {
|
walk(item.children);
|
} else {
|
result.push({
|
name: item.label || item.productName || "",
|
value: item.id || item.value,
|
});
|
}
|
});
|
};
|
walk(nodes);
|
return result;
|
};
|
|
const loadCategories = () => {
|
productTreeList()
|
.then(res => {
|
const nodes = Array.isArray(res) ? res : res?.data || [];
|
categoryActions.value = flattenTree(nodes);
|
})
|
.catch(() => {
|
categoryActions.value = [];
|
});
|
};
|
|
const openCategory = () => {
|
if (!categoryActions.value.length) loadCategories();
|
showCategorySheet.value = true;
|
};
|
|
const openModel = () => {
|
if (!props.row.productCategoryId) {
|
uni.showToast({ title: "请先选择产品大类", icon: "none" });
|
return;
|
}
|
if (!modelActions.value.length) {
|
uni.showToast({ title: "暂无规格型号", icon: "none" });
|
return;
|
}
|
showModelSheet.value = true;
|
};
|
|
const openTaxRate = () => {
|
if (!taxRateActions.value.length) {
|
uni.showToast({ title: "暂无税率选项", icon: "none" });
|
return;
|
}
|
showTaxRateSheet.value = true;
|
};
|
|
const openInvoiceType = () => {
|
showInvoiceSheet.value = true;
|
};
|
|
const onSelectCategory = action => {
|
showCategorySheet.value = false;
|
props.row.productCategoryId = action.value;
|
props.row.productCategory = action.name;
|
props.row.productModelId = "";
|
props.row.specificationModel = "";
|
props.row.unit = "";
|
modelActions.value = [];
|
modelList({ id: action.value })
|
.then(res => {
|
const rows = res?.data?.records || res?.data || res?.records || res || [];
|
modelActions.value = (Array.isArray(rows) ? rows : []).map(item => ({
|
name: item.model || item.specification || "",
|
value: item.id,
|
unit: item.unit,
|
}));
|
})
|
.catch(() => {
|
modelActions.value = [];
|
});
|
};
|
|
const onSelectModel = action => {
|
showModelSheet.value = false;
|
props.row.productModelId = action.value;
|
props.row.specificationModel = action.name;
|
if (action.unit) props.row.unit = action.unit;
|
};
|
|
const onSelectTaxRate = action => {
|
showTaxRateSheet.value = false;
|
props.row.taxRate = action.value;
|
};
|
|
const onSelectInvoiceType = action => {
|
showInvoiceSheet.value = false;
|
props.row.invoiceType = action.value;
|
};
|
|
// 打开型号面板前先把已有大类对应的型号捞回来(编辑回显场景)
|
onMounted(() => {
|
if (!props.row.productCategoryId) return;
|
modelList({ id: props.row.productCategoryId })
|
.then(res => {
|
const rows = res?.data?.records || res?.data || res?.records || res || [];
|
modelActions.value = (Array.isArray(rows) ? rows : []).map(item => ({
|
name: item.model || item.specification || "",
|
value: item.id,
|
unit: item.unit,
|
}));
|
// 只存了型号名称时反查 id,保证选择器能命中
|
if (!props.row.productModelId && props.row.specificationModel) {
|
const matched = modelActions.value.find(
|
item => item.name === props.row.specificationModel
|
);
|
if (matched) props.row.productModelId = matched.value;
|
}
|
})
|
.catch(() => {});
|
});
|
</script>
|
|
<style scoped lang="scss">
|
.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;
|
}
|
|
.product-body {
|
padding-top: 4px;
|
}
|
|
.field-row {
|
display: flex;
|
align-items: center;
|
min-height: 44px;
|
border-bottom: 1px solid #f5f6fa;
|
}
|
|
.field-row:last-child {
|
border-bottom: none;
|
}
|
|
.field-label {
|
width: 96px;
|
flex-shrink: 0;
|
font-size: 14px;
|
color: #606266;
|
}
|
|
.field-value {
|
flex: 1;
|
min-width: 0;
|
font-size: 14px;
|
color: #303133;
|
text-align: right;
|
word-break: break-all;
|
}
|
|
.field-value.placeholder {
|
color: #c0c4cc;
|
}
|
|
.field-value.readonly {
|
color: #909399;
|
}
|
</style>
|