feat:物料档案-详情模块

main
黄伟杰 3 hours ago
parent 6b0ce95093
commit 10e1b25761

@ -2107,7 +2107,8 @@ export default {
dialogPurchaseCycleLabel: 'Purchase Cycle (Days)',
dialogBrandLabel: 'Brand',
dialogBrandPlaceholder: 'Please enter brand',
categoryTree: 'Category Tree',
images: 'Image',
categoryTree: 'Material Category',
addCategory: 'Add Category',
refreshTree: 'Refresh'
},

@ -3275,7 +3275,8 @@ export default {
dialogPurchaseCycleLabel: '采购周期(天)',
dialogBrandLabel: '品牌',
dialogBrandPlaceholder: '请输入品牌',
categoryTree: '产品分类树',
images: '图片',
categoryTree: '物料分类',
addCategory: '新增分类',
refreshTree: '刷新'
},

@ -179,24 +179,34 @@
</el-form-item>
</el-col>
<el-col v-if="formType === 'update'" :span="24">
<el-form-item :label="t('FactoryModeling.ProductInformation.qrcode')" prop="qrcodeUrl">
<QrcodeActionCard
:image-url="formData.qrcodeUrl"
:print-id="formData.id"
:print-template-type="1"
:print-title="`${formData.name || '产品'}二维码打印预览`"
:empty-text="t('FactoryModeling.ProductInformation.qrcodeEmpty')"
:refresh-url="getQrcodeRefreshUrl()"
:refresh-disabled="!formData.id || !formData.barCode"
refresh-confirm-text="确认刷新该产品二维码吗?"
:template-json="formData.templateJson"
:print-data="buildPrintData()"
@refresh-success="handleQrcodeRefreshSuccess"
/>
<div class="flex items-start gap-20px">
<el-form-item :label="t('FactoryModeling.ProductInformation.qrcode')" prop="qrcodeUrl" class="flex-1">
<QrcodeActionCard
:image-url="formData.qrcodeUrl"
:print-id="formData.id"
:print-template-type="1"
:print-title="`${formData.name || '产品'}二维码打印预览`"
:empty-text="t('FactoryModeling.ProductInformation.qrcodeEmpty')"
:refresh-url="getQrcodeRefreshUrl()"
:refresh-disabled="!formData.id || !formData.barCode"
refresh-confirm-text="确认刷新该产品二维码吗?"
:template-json="formData.templateJson"
:print-data="buildPrintData()"
@refresh-success="handleQrcodeRefreshSuccess"
/>
</el-form-item>
<el-form-item :label="t('FactoryModeling.ProductInformation.images')" prop="images" class="image-form-item flex-1">
<UploadImg v-model="formData.images" />
</el-form-item>
</div>
</el-col>
<el-col v-else :span="24">
<el-form-item :label="t('FactoryModeling.ProductInformation.images')" prop="images" class="image-form-item">
<UploadImg v-model="formData.images" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item :label="t('FactoryModeling.ProductInformation.dialogRemarkLabel')" prop="remark">
<el-form-item :label="t('FactoryModeling.ProductInformation.dialogRemarkLabel')" prop="remark" style="margin-top: 8px;">
<el-input type="textarea" v-model="formData.remark" :placeholder="t('FactoryModeling.ProductInformation.dialogRemarkPlaceholder')" />
</el-form-item>
</el-col>
@ -447,6 +457,7 @@ const formData = ref({
fragileFlag: undefined as number | undefined,
purchaseCycle: undefined as number | undefined,
brand: undefined as string | undefined,
images: undefined as string | undefined,
sparePartLevel: undefined as number | undefined
})
const selectedDeviceRows = ref<any[]>([])
@ -922,8 +933,8 @@ const open = async (type: string, id?: number) => {
sparePartLevel: productData.sparePartLevel != null ? Number(productData.sparePartLevel) : undefined,
devices,
molds,
packagingSchemes: (productData as any).packagingSchemes || [],
suppliers: (productData as any).suppliers || []
packagingSchemes: normalizeDefaultStatus((productData as any).packagingSchemes, (productData as any).defaultPackagingSchemeId, 'packagingSchemeId'),
suppliers: normalizeDefaultStatus((productData as any).suppliers, (productData as any).defaultSupplierId, 'supplierId')
}
selectedDeviceRows.value = toDeviceRows(devices)
selectedMoldRows.value = toMoldRows(molds)
@ -1005,6 +1016,24 @@ const buildRelationIdListString = (list: { id: number; name: string }[]) => {
return list.map((item) => Number(item.id)).filter((id) => Number.isFinite(id))
}
const getDefaultPackagingSchemeId = () => {
const defaultItem = formData.value.packagingSchemes?.find((item) => item.defaultStatus === 1)
return defaultItem?.packagingSchemeId ?? undefined
}
const getDefaultSupplierId = () => {
const defaultItem = formData.value.suppliers?.find((item) => item.defaultStatus === 1)
return defaultItem?.supplierId ?? undefined
}
const normalizeDefaultStatus = (list: any[] | undefined, defaultId: number | undefined, idKey: string) => {
if (!list?.length) return []
return list.map((item: any) => ({
...item,
defaultStatus: defaultId != null && item[idKey] === defaultId ? 1 : 0
}))
}
const submitForm = async () => {
await formRef.value.validate()
formLoading.value = true
@ -1014,7 +1043,9 @@ const submitForm = async () => {
const data = {
...formData.value,
deviceIds: buildRelationIdListString(relationDevices),
moldIds: buildRelationIdListString(relationMolds)
moldIds: buildRelationIdListString(relationMolds),
defaultPackagingSchemeId: formData.value.categoryType === 1 ? getDefaultPackagingSchemeId() : undefined,
defaultSupplierId: (formData.value.categoryType === 2 || formData.value.categoryType === 3) ? getDefaultSupplierId() : undefined
} as unknown as ProductVO
delete (data as any).devices
delete (data as any).molds
@ -1115,6 +1146,15 @@ watch(
border-top: 1px solid #ebeef5;
}
.image-form-item {
margin-bottom: 28px;
:deep(.el-form-item__content) {
min-height: 150px;
align-items: flex-start;
}
}
@media (max-width: 768px) {
.dv-repair-panel__header {
padding: 14px 16px;

@ -80,10 +80,16 @@
</ContentWrap>
<!-- 右侧产品表格 -->
<ContentWrap class="product-page-right">
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<div class="product-page-right">
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true" :row-class-name="getRowClassName">
<el-table-column :label="t('FactoryModeling.ProductInformation.tableBarCodeColumn')" align="center" prop="barCode"
sortable />
<el-table-column :label="t('FactoryModeling.ProductInformation.dialogCategoryTypeLabel')" align="center" prop="categoryType" sortable>
<template #default="scope">
<dict-tag :type="DICT_TYPE.MATERIAL_CLASSIFICATION_TYPE" :value="scope.row.categoryType" />
</template>
</el-table-column>
<el-table-column :label="t('FactoryModeling.ProductInformation.tableNameColumn')" align="left" prop="name"
width="220px" sortable />
<el-table-column :label="t('FactoryModeling.ProductInformation.tableStandardColumn')" align="center"
@ -100,8 +106,11 @@
</el-table-column>
<el-table-column :label="t('FactoryModeling.ProductInformation.tableCreateTimeColumn')" align="center"
prop="createTime" :formatter="dateFormatter" width="180px" sortable />
<el-table-column :label="t('FactoryModeling.ProductInformation.tableOperateColumn')" align="center" width="150px">
<el-table-column :label="t('FactoryModeling.ProductInformation.tableOperateColumn')" align="center" width="210px">
<template #default="scope">
<el-button link type="primary" @click="handleShowDetail(scope.row.id)">
详情
</el-button>
<el-button link type="primary" @click="openForm('update', scope.row.id)" v-hasPermi="['erp:product:update']">
{{ t('FactoryModeling.ProductInformation.tableEditAction') }}
</el-button>
@ -111,10 +120,178 @@
</template>
</el-table-column>
</el-table>
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
<Pagination :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
@pagination="getList" />
</ContentWrap>
<ContentWrap v-if="detailVisible" class="product-detail-wrap" v-loading="detailLoading">
<template v-if="productDetail">
<div class="detail-header">
<div class="detail-title">物料详情</div>
<el-button text @click="closeDetail">
<Icon icon="ep:close" />
</el-button>
</div>
<div class="detail-card">
<div class="detail-image-box">
<div class="detail-image-item">
<div class="detail-image-label">{{ t('FactoryModeling.ProductInformation.qrcode') }}</div>
<el-image
v-if="productDetail.qrcodeUrl"
class="detail-image"
:src="productDetail.qrcodeUrl"
fit="contain"
:preview-src-list="[productDetail.qrcodeUrl]"
preview-teleported
/>
<el-empty v-else :image-size="64" :description="t('FactoryModeling.ProductInformation.qrcodeEmpty')" />
</div>
</div>
<div class="detail-info">
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogBarCodeLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.barCode) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogCategoryTypeLabel') }}</span>
<span class="field-value">{{ formatCategoryType(productDetail.categoryType) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogNameLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.name) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogCategoryLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.subCategoryName) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogUnitLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.unitName) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogStandardLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.standard || productDetail.deviceSpec || productDetail.model) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogExpiryDayLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.expiryDay) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogStatusLabel') }}</span>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="productDetail.status" />
</div>
<!-- 产品类型关联设备关联模具 -->
<template v-if="productDetail.categoryType === 1">
<div class="detail-field">
<span class="field-label">关联设备</span>
<span class="field-value">{{ formatDeviceText(productDetail) }}</span>
</div>
<div class="detail-field">
<span class="field-label">关联模具</span>
<span class="field-value">{{ formatMoldText(productDetail) }}</span>
</div>
</template>
<!-- 物料/备件类型供应商 -->
<template v-if="productDetail.categoryType === 2 || productDetail.categoryType === 3">
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogSupplierLabel') }}</span>
<span class="field-value">{{ formatSupplierText(productDetail) }}</span>
</div>
</template>
<!-- 备件类型备件等级是否易损件采购周期品牌 -->
<template v-if="productDetail.categoryType === 3">
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogSparePartLevelLabel') }}</span>
<span class="field-value">{{ formatSparePartLevel(productDetail.sparePartLevel) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogFragileFlagLabel') }}</span>
<span class="field-value">{{ productDetail.fragileFlag === 1 ? t('common.yes') : productDetail.fragileFlag === 0 ? t('common.no') : '-' }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogPurchaseCycleLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.purchaseCycle) }}</span>
</div>
<div class="detail-field">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogBrandLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.brand) }}</span>
</div>
</template>
<div class="detail-field detail-field--full">
<span class="field-label">{{ t('FactoryModeling.ProductInformation.dialogRemarkLabel') }}</span>
<span class="field-value">{{ formatValue(productDetail.remark) }}</span>
</div>
</div>
<div class="detail-summary">
<div class="section-title">{{ detailSummaryTitle }}</div>
<!-- 产品类型包装方案摘要 -->
<template v-if="productDetail.categoryType === 1">
<div class="summary-item">
<Icon icon="ep:box" class="summary-icon" />
<span>{{ t('FactoryModeling.ProductInformation.dialogPackagingSchemeLabel') }}{{ packagingSchemeCount }} </span>
</div>
<div class="summary-item">
<Icon icon="ep:document-checked" class="summary-icon" />
<span>默认参考方案{{ defaultPackagingSchemeText }}</span>
</div>
</template>
<!-- 物料/备件类型供应商摘要 -->
<template v-if="productDetail.categoryType === 2 || productDetail.categoryType === 3">
<div class="summary-item">
<Icon icon="ep:office-building" class="summary-icon" />
<span>{{ t('FactoryModeling.ProductInformation.dialogSupplierLabel') }}{{ supplierCount }} </span>
</div>
<div class="summary-item">
<Icon icon="ep:star" class="summary-icon" />
<span>默认供应商{{ defaultSupplierText }}</span>
</div>
</template>
<div class="detail-image-item" style="margin-top: 12px;">
<div class="detail-image-label">{{ t('FactoryModeling.ProductInformation.images') }}</div>
<el-image
v-if="productDetail.images"
class="detail-image"
:src="productDetail.images"
fit="contain"
:preview-src-list="[productDetail.images]"
preview-teleported
/>
<el-empty v-else :image-size="64" description="暂无图片" />
</div>
</div>
<!-- 产品类型包装方案表格 -->
<div v-if="productDetail.categoryType === 1" class="detail-table-box">
<div class="detail-table-title">
<span>{{ t('FactoryModeling.ProductInformation.dialogPackagingSchemeLabel') }}</span>
<span> {{ packagingSchemeCount }} </span>
</div>
<el-table :data="productDetail.packagingSchemes || []" border size="small" :show-overflow-tooltip="true">
<el-table-column :label="t('ErpStock.PackagingScheme.name')" prop="packagingSchemeName" min-width="130" />
<el-table-column :label="t('ErpStock.PackagingScheme.packageQuantity')" prop="packageQuantity" min-width="100" />
<el-table-column :label="t('ErpStock.PackagingScheme.palletPackageQuantity')" prop="palletPackageQuantity" min-width="100" />
</el-table>
</div>
<!-- 物料/备件类型供应商表格 -->
<div v-else-if="productDetail.categoryType === 2 || productDetail.categoryType === 3" class="detail-table-box">
<div class="detail-table-title">
<span>{{ t('FactoryModeling.ProductInformation.dialogSupplierLabel') }}</span>
<span> {{ supplierCount }} </span>
</div>
<el-table :data="productDetail.suppliers || []" border size="small" :show-overflow-tooltip="true">
<el-table-column :label="t('ErpPurchase.Supplier.name')" prop="supplierName" min-width="160" />
<el-table-column :label="t('ErpPurchase.Supplier.contact')" prop="supplierContact" min-width="100" />
<el-table-column :label="t('ErpPurchase.Supplier.mobile')" prop="supplierMobile" min-width="120" />
</el-table>
</div>
</div>
</template>
</ContentWrap>
</div>
</div>
</template>
<ProductForm v-else-if="formType === 'product'" ref="formRef" @success="getList" @closed="handleFormClosed" />
@ -126,7 +303,7 @@
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import { dateFormatter, formatDate } from '@/utils/formatTime'
import download from '@/utils/download'
import { ProductApi, ProductVO } from '@/api/erp/product/product'
import { ProductCategoryApi } from '@/api/erp/product/category'
@ -156,6 +333,10 @@ const queryFormRef = ref()
const exportLoading = ref(false)
const formVisible = ref(false)
const formType = ref('')
const detailVisible = ref(false)
const detailLoading = ref(false)
const currentDetailId = ref<number>()
const productDetail = ref<any>()
/** 分类树数据 */
const groupTreeData = ref<any[]>([])
@ -226,6 +407,9 @@ const getList = async () => {
const data = await ProductApi.getProductPage(queryParams)
list.value = data.list
total.value = data.total
if (currentDetailId.value && !list.value.some((item) => item.id === currentDetailId.value)) {
closeDetail()
}
} finally {
loading.value = false
}
@ -233,11 +417,13 @@ const getList = async () => {
const handleQuery = () => {
queryParams.pageNo = 1
closeDetail()
getList()
}
const resetQuery = () => {
queryFormRef.value.resetFields()
closeDetail()
handleQuery()
}
@ -255,6 +441,9 @@ const handleDelete = async (id: number) => {
await message.delConfirm()
await ProductApi.deleteProduct(id)
message.success(t('common.delSuccess'))
if (currentDetailId.value === id) {
closeDetail()
}
await getList()
} catch { }
}
@ -293,6 +482,98 @@ const handleFormClosed = () => {
formVisible.value = false
formType.value = ''
}
const handleShowDetail = async (id: number) => {
detailVisible.value = true
currentDetailId.value = id
detailLoading.value = true
try {
productDetail.value = await ProductApi.getProduct(id)
} finally {
detailLoading.value = false
}
}
const closeDetail = () => {
detailVisible.value = false
detailLoading.value = false
currentDetailId.value = undefined
productDetail.value = undefined
}
const getRowClassName = ({ row }: { row: ProductVO }) => {
return row.id === currentDetailId.value ? 'current-detail-row' : ''
}
const packagingSchemes = computed(() => productDetail.value?.packagingSchemes || [])
const packagingSchemeCount = computed(() => packagingSchemes.value.length)
const defaultPackagingScheme = computed(() => {
const defaultId = productDetail.value?.defaultPackagingSchemeId
return packagingSchemes.value.find((item: any) => item.defaultStatus === 1)
|| packagingSchemes.value.find((item: any) => item.packagingSchemeId === defaultId)
})
const defaultPackagingSchemeText = computed(() => {
const scheme = defaultPackagingScheme.value
if (!scheme) return '-'
return formatValue(scheme.packagingSchemeName)
})
const detailSummaryTitle = computed(() => {
const categoryType = formatCategoryType(productDetail.value?.categoryType)
return categoryType === '-' ? '物料属性摘要' : `${categoryType}属性摘要`
})
//
const suppliers = computed(() => productDetail.value?.suppliers || [])
const supplierCount = computed(() => suppliers.value.length)
const defaultSupplier = computed(() => {
return suppliers.value.find((item: any) => item.defaultStatus === 1)
})
const defaultSupplierText = computed(() => {
const supplier = defaultSupplier.value
if (!supplier) return '-'
return supplier.supplierName || '-'
})
//
const formatDeviceText = (detail: any) => {
if (!detail?.devices?.length && !detail?.deviceList?.length) return '-'
const list = detail.devices || detail.deviceList || []
return list.map((item: any) => item.name || item.deviceName || `ID:${item.id}`).join('、') || '-'
}
const formatMoldText = (detail: any) => {
if (!detail?.molds?.length && !detail?.moldList?.length) return '-'
const list = detail.molds || detail.moldList || []
return list.map((item: any) => item.name || item.code || `ID:${item.id}`).join('、') || '-'
}
const formatSupplierText = (detail: any) => {
if (!detail?.suppliers?.length) return '-'
return detail.suppliers.map((item: any) => item.supplierName || `ID:${item.supplierId}`).join('、') || '-'
}
const formatSparePartLevel = (value: any) => {
if (value === undefined || value === null || value === '') return '-'
const dictItem = sparePartLevelDict.value.find((item: any) => item.value === Number(value))
return dictItem?.label || formatValue(value)
}
const sparePartLevelDict = computed(() => getIntDictOptions(DICT_TYPE.SPARE_PARTS_LEVEL))
const formatValue = (value: any) => {
return value === undefined || value === null || value === '' ? '-' : value
}
const formatCategoryType = (value: any) => {
const dictItem = typeDict.value.find((item: any) => item.value === Number(value))
return dictItem?.label || formatValue(value)
}
const formatDateTime = (value: any) => {
if (value === undefined || value === null || value === '') return '-'
return formatDate(value)
}
</script>
<style lang="scss" scoped>
@ -311,6 +592,145 @@ const handleFormClosed = () => {
min-width: 0;
}
.product-detail-wrap {
margin-top: 12px;
}
:deep(.current-detail-row) {
--el-table-tr-bg-color: var(--el-color-primary-light-9);
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.detail-title {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.detail-card {
display: grid;
grid-template-columns: 200px 1fr 300px 1fr;
gap: 16px;
padding: 16px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
}
.detail-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px 28px;
align-content: start;
}
.detail-field {
display: flex;
min-width: 0;
font-size: 14px;
line-height: 24px;
}
.detail-field--full {
grid-column: 1 / -1;
}
.field-label {
flex: 0 0 auto;
color: var(--el-text-color-regular);
}
.field-value {
min-width: 0;
color: var(--el-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.detail-image-box {
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
}
.detail-image-item {
display: flex;
flex-direction: column;
align-items: left;
gap: 6px;
}
.detail-image-label {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.detail-image {
width: 170px;
height: 170px;
}
.detail-summary {
min-width: 0;
}
.section-title {
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.summary-item {
display: flex;
gap: 10px;
align-items: center;
min-height: 32px;
font-size: 14px;
color: var(--el-text-color-primary);
}
.summary-icon {
flex: 0 0 auto;
color: var(--el-color-primary);
}
.detail-table-box {
min-width: 0;
}
.detail-table-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-primary);
span:last-child {
color: var(--el-text-color-secondary);
}
}
@media (max-width: 1200px) {
.detail-card {
grid-template-columns: 1fr 1fr;
}
.detail-summary,
.detail-table-box {
grid-column: 1 / -1;
}
}
.tree-header {
display: flex;
align-items: center;

Loading…
Cancel
Save