成品图像上传初实现,支持多图,检测报告的图片以及链接上传
This commit is contained in:
@ -13,18 +13,38 @@
|
||||
<template #reference><el-button :icon="Setting" class="action-btn">表头</el-button></template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="c in allColumns" :key="c.prop"><el-checkbox :value="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
<el-col :span="8" v-for="c in allColumns" :key="c.prop"><el-checkbox :label="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%" class="modern-table" header-cell-class-name="table-header-gray">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
class="modern-table"
|
||||
header-cell-class-name="table-header-gray"
|
||||
>
|
||||
<template v-for="col in allColumns" :key="col.prop">
|
||||
<el-table-column v-if="visibleColumnProps.includes(col.prop)" :prop="col.prop" :label="col.label" :min-width="col.minWidth || '120'" show-overflow-tooltip>
|
||||
<el-table-column
|
||||
v-if="visibleColumnProps.includes(col.prop)"
|
||||
:prop="col.prop"
|
||||
:label="col.label"
|
||||
:min-width="col.minWidth || '110'"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
|
||||
<template #default="scope" v-if="['serial_number'].includes(col.prop)">
|
||||
<template #default="scope" v-if="col.prop === 'material_name'">
|
||||
<span class="clickable-text" @click="handleUpdate(scope.row)">
|
||||
{{ scope.row.material_name }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['serial_number'].includes(col.prop)">
|
||||
<span v-if="scope.row[col.prop]" class="tag-sn">{{ scope.row[col.prop] }}</span>
|
||||
<span v-else class="text-placeholder">-</span>
|
||||
</template>
|
||||
@ -41,7 +61,28 @@
|
||||
<el-tag :type="getQualityType(scope.row.quality_status)" effect="dark" size="small">{{ scope.row.quality_status }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['quality_report_link', 'detail_link', 'inspection_report_link'].includes(col.prop)">
|
||||
<template #default="scope" v-else-if="['product_photo', 'quality_report_link', 'inspection_report_link'].includes(col.prop)">
|
||||
<div v-if="getImagesOnly(scope.row[col.prop]).length > 0" style="display: flex; align-items: center; justify-content: center;">
|
||||
<el-image
|
||||
style="width: 40px; height: 40px; border-radius: 4px; border: 1px solid #dcdfe6; cursor: zoom-in;"
|
||||
:src="getImageUrl(getImagesOnly(scope.row[col.prop])[0])"
|
||||
:preview-src-list="getImagesOnly(scope.row[col.prop]).map(u => getImageUrl(u))"
|
||||
preview-teleported
|
||||
fit="cover"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-slot"><el-icon><Picture /></el-icon></div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span v-if="getImagesOnly(scope.row[col.prop]).length > 1" class="more-images-badge">+{{getImagesOnly(scope.row[col.prop]).length}}</span>
|
||||
</div>
|
||||
<div v-else-if="hasExternalLink(scope.row[col.prop])" style="text-align: center;">
|
||||
<el-tag size="small" type="info"><el-icon><Link /></el-icon> 链接</el-tag>
|
||||
</div>
|
||||
<span v-else class="text-placeholder">-</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['detail_link'].includes(col.prop)">
|
||||
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" :underline="false">
|
||||
<el-icon><Link /></el-icon> 查看
|
||||
</el-link>
|
||||
@ -54,10 +95,10 @@
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||||
<el-icon><Printer/></el-icon> 打印
|
||||
<el-icon><Printer/></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除?" @confirm="handleDelete(row)"><template #reference><el-button link type="danger">删除</el-button></template></el-popconfirm>
|
||||
@ -68,134 +109,206 @@
|
||||
<el-pagination class="pagination-bar" v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" background @change="fetchData" />
|
||||
|
||||
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="1100px" top="5vh" :close-on-click-modal="false" class="stylish-dialog">
|
||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="large" class="stylish-form">
|
||||
<div class="dialog-scroll-container">
|
||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||||
|
||||
<div class="form-card basic-card">
|
||||
<div class="card-title"><el-icon class="icon"><Box /></el-icon><span>1. 基础信息</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||||
<el-col :span="10">
|
||||
<el-form-item label="物料搜索" prop="base_id">
|
||||
<el-select
|
||||
v-model="form.base_id"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="搜名称/规格..."
|
||||
:remote-method="handleSearchMaterial"
|
||||
@visible-change="handleMaterialDropdownVisible"
|
||||
:loading="searchLoading"
|
||||
style="width: 100%"
|
||||
@change="onMaterialSelected"
|
||||
default-first-option
|
||||
>
|
||||
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
|
||||
<div class="option-item">
|
||||
<span class="opt-name">{{ item.name }}</span>
|
||||
<span class="opt-spec">{{ item.spec }}</span>
|
||||
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
|
||||
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
|
||||
<div class="form-card basic-card">
|
||||
<div class="card-title"><el-icon class="icon"><Box /></el-icon><span>1. 基础信息</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||||
<el-col :span="10">
|
||||
<el-form-item label="物料搜索" prop="base_id">
|
||||
<el-select
|
||||
v-model="form.base_id"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
placeholder="搜名称/规格..."
|
||||
:remote-method="handleSearchMaterial"
|
||||
@visible-change="handleMaterialDropdownVisible"
|
||||
:loading="searchLoading"
|
||||
style="width: 100%"
|
||||
@change="onMaterialSelected"
|
||||
default-first-option
|
||||
>
|
||||
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
|
||||
<div class="option-item">
|
||||
<span class="opt-name">{{ item.name }}</span>
|
||||
<span class="opt-spec">{{ item.spec }}</span>
|
||||
<el-tag v-if="item.isHistory" size="small" type="info" effect="plain">历史</el-tag>
|
||||
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="14" style="display: flex; align-items: center;">
|
||||
<span class="search-tip">
|
||||
<el-icon><InfoFilled /></el-icon> 未输入时展示最新物料;输入关键词进行精确搜索。
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card inbound-card">
|
||||
<div class="card-title"><el-icon class="icon"><House /></el-icon><span>2. 入库详情</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6"><el-form-item label="SKU" prop="sku"><el-input v-model="form.sku" placeholder="自动生成" disabled /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="自动生成" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="入库日期"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="identity-panel">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号(SN)" prop="serial_number">
|
||||
<el-input v-model="form.serial_number" placeholder="必填: 唯一序列号" clearable><template #prefix><span class="prefix-tag sn">SN</span></template></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="入库数量" prop="in_quantity">
|
||||
<el-input-number v-model="form.in_quantity" :min="1" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="24" style="margin-top:15px">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="质量状态">
|
||||
<el-select v-model="form.quality_status" style="width:100%">
|
||||
<el-option label="合格" value="合格" /><el-option label="不合格" value="不合格" /><el-option label="待检" value="待检" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<el-form-item label="成品实拍" prop="product_photo">
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
v-model:file-list="productPhotoList"
|
||||
action="#"
|
||||
list-type="picture-card"
|
||||
multiple
|
||||
:http-request="(opts) => customUpload(opts, 'product_photo')"
|
||||
:on-preview="handlePreviewPicture"
|
||||
:on-remove="(file) => handleRemoveImage(file, 'product_photo')"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="camera-card" @click="triggerCamera('product_photo')">
|
||||
<el-icon><Camera /></el-icon><span class="text">拍照</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="14" style="display: flex; align-items: center;">
|
||||
<span class="search-tip">
|
||||
<el-icon><InfoFilled /></el-icon> 未输入时展示最新物料;输入关键词进行精确搜索。
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card inbound-card">
|
||||
<div class="card-title"><el-icon class="icon"><House /></el-icon><span>2. 入库详情</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6"><el-form-item label="SKU" prop="sku"><el-input v-model="form.sku" placeholder="自动生成" disabled /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" placeholder="自动生成" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="入库日期"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="identity-panel">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号(SN)" prop="serial_number">
|
||||
<el-input v-model="form.serial_number" placeholder="必填: 唯一序列号" clearable><template #prefix><span class="prefix-tag sn">SN</span></template></el-input>
|
||||
</div>
|
||||
<el-input v-model="form.product_photo" style="display:none" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="入库数量" prop="in_quantity">
|
||||
<el-input-number v-model="form.in_quantity" :min="1" style="width:100%" />
|
||||
<el-form-item label="质量报告" prop="quality_report_link">
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
v-model:file-list="qualityFileList"
|
||||
action="#"
|
||||
list-type="picture-card"
|
||||
multiple
|
||||
:http-request="(opts) => customUpload(opts, 'quality_report_link')"
|
||||
:on-preview="handlePreviewPicture"
|
||||
:on-remove="(file) => handleRemoveImage(file, 'quality_report_link')"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="camera-card" @click="triggerCamera('quality_report_link')">
|
||||
<el-icon><Camera /></el-icon><span class="text">拍照</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="quality_url" placeholder="外部链接..." style="margin-top:8px" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
||||
<el-input v-model="form.quality_report_link" style="display:none" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="检测报告" prop="inspection_report_link">
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
v-model:file-list="inspectionFileList"
|
||||
action="#"
|
||||
list-type="picture-card"
|
||||
multiple
|
||||
:http-request="(opts) => customUpload(opts, 'inspection_report_link')"
|
||||
:on-preview="handlePreviewPicture"
|
||||
:on-remove="(file) => handleRemoveImage(file, 'inspection_report_link')"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="camera-card" @click="triggerCamera('inspection_report_link')">
|
||||
<el-icon><Camera /></el-icon><span class="text">拍照</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="inspection_url" placeholder="外部链接..." style="margin-top:8px" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
||||
<el-input v-model="form.inspection_report_link" style="display:none" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="24" style="margin-top:15px">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="质量状态">
|
||||
<el-select v-model="form.quality_status" style="width:100%">
|
||||
<el-option label="合格" value="合格" /><el-option label="不合格" value="不合格" /><el-option label="待检" value="待检" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="9"><el-form-item label="质量报告"><el-input v-model="form.quality_report_link" placeholder="链接" /></el-form-item></el-col>
|
||||
<el-col :span="9"><el-form-item label="检测报告"><el-input v-model="form.inspection_report_link" placeholder="产品检测报告链接" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card production-card">
|
||||
<div class="card-title"><el-icon class="icon"><Setting /></el-icon><span>3. 生产与销售信息</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="订单号"><el-input v-model="form.order_id" placeholder="关联销售订单" /></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="负责人">
|
||||
<el-autocomplete
|
||||
v-model="form.production_manager"
|
||||
:fetch-suggestions="querySearchManager"
|
||||
placeholder="输入或选择负责人"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
:trigger-on-focus="true"
|
||||
@select="handleManagerSelect"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="产品定价"><el-input-number v-model="form.sale_price" :precision="2" style="width:100%"><template #prefix>¥</template></el-input-number></el-form-item></el-col>
|
||||
</el-row>
|
||||
<div class="form-card production-card">
|
||||
<div class="card-title"><el-icon class="icon"><Setting /></el-icon><span>3. 生产与销售信息</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="订单号"><el-input v-model="form.order_id" placeholder="关联销售订单" /></el-form-item></el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="负责人">
|
||||
<el-autocomplete
|
||||
v-model="form.production_manager"
|
||||
:fetch-suggestions="querySearchManager"
|
||||
placeholder="输入或选择负责人"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
:trigger-on-focus="true"
|
||||
@select="handleManagerSelect"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8"><el-form-item label="产品定价"><el-input-number v-model="form.sale_price" :precision="2" style="width:100%"><template #prefix>¥</template></el-input-number></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生产时间">
|
||||
<el-date-picker v-model="form.production_time_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="原料成本"><el-input-number v-model="form.raw_material_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人工成本"><el-input-number v-model="form.manual_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin-top:10px">
|
||||
<el-col :span="24"><el-form-item label="详情链接"><el-input v-model="form.detail_link" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生产时间">
|
||||
<el-date-picker v-model="form.production_time_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="原料成本"><el-input-number v-model="form.raw_material_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人工成本"><el-input-number v-model="form.manual_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin-top:10px">
|
||||
<el-col :span="24"><el-form-item label="详情链接"><el-input v-model="form.detail_link" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@ -207,19 +320,18 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="printVisible"
|
||||
title="标签打印预览"
|
||||
width="400px"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<input type="file" ref="cameraInputRef" accept="image/*" capture="environment" style="display: none" @change="handleCameraFile" />
|
||||
|
||||
<el-dialog v-model="dialogVisibleImage" append-to-body width="50%">
|
||||
<img style="width: 100%" :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="printVisible" title="标签打印预览" width="400px" destroy-on-close append-to-body>
|
||||
<div style="text-align: center;">
|
||||
<div v-loading="printLoading" class="preview-box">
|
||||
<img v-if="previewUrl" :src="previewUrl" alt="Label Preview" style="width: 100%; border: 1px solid #ccc;"/>
|
||||
<div v-else class="empty-preview">正在生成预览...</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; font-size: 14px; color: #666;">
|
||||
<p>打印机 IP: 192.168.9.205</p>
|
||||
<p>尺寸: 40mm x 30mm</p>
|
||||
@ -240,10 +352,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer } from '@element-plus/icons-vue'
|
||||
import { Plus, Setting, Refresh, Search, Box, House, Link, InfoFilled, Printer, Camera, Picture } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
import { getProductList, createProductInbound, updateProductInbound, deleteProductInbound, searchMaterialBase } from '@/api/inbound/product'
|
||||
import { uploadFile, deleteFile } from '@/api/inbound/buy'
|
||||
import { getLabelPreview, executePrint } from '@/api/common/print'
|
||||
|
||||
const loading = ref(false)
|
||||
@ -264,27 +377,63 @@ const printing = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const currentPrintData = ref<any>({})
|
||||
|
||||
// 图片/拍照相关
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisibleImage = ref(false)
|
||||
// 3个独立的列表
|
||||
const productPhotoList = ref<any[]>([]) // 成品实拍
|
||||
const qualityFileList = ref<any[]>([]) // 质量报告
|
||||
const inspectionFileList = ref<any[]>([]) // 检测报告
|
||||
|
||||
const cameraInputRef = ref<HTMLInputElement | null>(null)
|
||||
const currentCameraField = ref<'product_photo' | 'quality_report_link' | 'inspection_report_link'>('product_photo')
|
||||
const quality_url = ref('')
|
||||
const inspection_url = ref('')
|
||||
|
||||
// [核心优化] 所有列定义
|
||||
const allColumns = [
|
||||
{ prop: 'material_name', label: '名称', minWidth: '120' },
|
||||
{ prop: 'material_name', label: '名称', minWidth: '140' },
|
||||
{ prop: 'sku', label: 'SKU', minWidth: '110' },
|
||||
{ prop: 'serial_number', label: '序列号', minWidth: '130' },
|
||||
{ prop: 'qty_stock', label: '库存', minWidth: '90' },
|
||||
{ prop: 'status', label: '状态', minWidth: '90' },
|
||||
{ prop: 'quality_status', label: '质量', minWidth: '90' },
|
||||
{ prop: 'spec_model', label: '规格', minWidth: '120' },
|
||||
{ prop: 'sku', label: 'SKU', minWidth: '100' },
|
||||
{ prop: 'serial_number', label: '序列号', minWidth: '140' },
|
||||
{ prop: 'qty_stock', label: '库存', minWidth: '80' },
|
||||
{ prop: 'status', label: '状态', minWidth: '80' },
|
||||
{ prop: 'quality_status', label: '质量', minWidth: '80' },
|
||||
{ prop: 'product_photo', label: '实拍图', minWidth: '100' },
|
||||
{ prop: 'sale_price', label: '售价', minWidth: '100' },
|
||||
{ prop: 'order_id', label: '订单号', minWidth: '120' },
|
||||
{ prop: 'work_order_code', label: '工单号', minWidth: '120' },
|
||||
{ prop: 'bom_code', label: 'BOM', minWidth: '100' },
|
||||
{ prop: 'inspection_report_link', label: '检测报告', minWidth: '100' },
|
||||
{ prop: 'quality_report_link', label: '质量报告', minWidth: '100' },
|
||||
{ prop: 'inspection_report_link', label: '检测报告', minWidth: '100' },
|
||||
{ prop: 'bom_code', label: 'BOM', minWidth: '100' },
|
||||
{ prop: 'production_manager', label: '负责人', minWidth: '100' },
|
||||
{ prop: 'raw_material_cost', label: '原料成本', minWidth: '100' },
|
||||
{ prop: 'manual_cost', label: '人工成本', minWidth: '100' },
|
||||
{ prop: 'inbound_date', label: '生产日期', minWidth: '120' }
|
||||
{ prop: 'inbound_date', label: '生产日期', minWidth: '120' },
|
||||
{ prop: 'detail_link', label: '详情', minWidth: '100' }
|
||||
]
|
||||
|
||||
const visibleColumnProps = ref(allColumns.map(c => c.prop))
|
||||
// [核心优化] 默认显示的列 (减少到核心几列,避免卡顿)
|
||||
const defaultVisibleCols = [
|
||||
'material_name', 'sku', 'serial_number', 'qty_stock', 'status', 'quality_status',
|
||||
'product_photo', 'sale_price', 'order_id'
|
||||
]
|
||||
|
||||
// 表头持久化
|
||||
const STORAGE_KEY = 'stock_product_visible_columns'
|
||||
const getSavedColumns = () => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
return saved ? JSON.parse(saved) : defaultVisibleCols
|
||||
} catch (e) {
|
||||
return defaultVisibleCols
|
||||
}
|
||||
}
|
||||
const visibleColumnProps = ref(getSavedColumns())
|
||||
|
||||
watch(visibleColumnProps, (newVal) => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(newVal))
|
||||
}, { deep: true })
|
||||
|
||||
const form = reactive({
|
||||
id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '',
|
||||
@ -294,7 +443,10 @@ const form = reactive({
|
||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
||||
production_manager: '', production_time_range: [] as string[],
|
||||
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
||||
quality_report_link: '', inspection_report_link: '', detail_link: ''
|
||||
quality_report_link: [] as string[],
|
||||
inspection_report_link: [] as string[],
|
||||
product_photo: [] as string[],
|
||||
detail_link: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
@ -303,133 +455,23 @@ const rules = {
|
||||
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 历史记录管理器 (Local Storage)
|
||||
// ------------------------------------
|
||||
const HISTORY_KEYS = {
|
||||
PRODUCTION_MANAGER: 'history_product_managers',
|
||||
MATERIAL: 'history_product_materials'
|
||||
}
|
||||
const HISTORY_KEYS = { PRODUCTION_MANAGER: 'history_product_managers', MATERIAL: 'history_product_materials' }
|
||||
const saveToHistory = (key: string, value: string) => { if (!value) return; try { const existing = localStorage.getItem(key); let list = existing ? JSON.parse(existing) : []; list = list.filter((i: string) => i !== value); list.unshift(value); if (list.length > 20) list = list.slice(0, 20); localStorage.setItem(key, JSON.stringify(list)) } catch (e) {} }
|
||||
const getHistoryList = (key: string): any[] => { try { return (JSON.parse(localStorage.getItem(key) || '[]')).map((v: string) => ({ value: v })) } catch (e) { return [] } }
|
||||
const saveMaterialHistory = (item: any) => { if (!item || !item.id) return; const key = HISTORY_KEYS.MATERIAL; try { let list = JSON.parse(localStorage.getItem(key) || '[]'); list = list.filter((i: any) => i.id !== item.id); list.unshift({ ...item, isHistory: true }); if (list.length > 10) list = list.slice(0, 10); localStorage.setItem(key, JSON.stringify(list)) } catch (e) {} }
|
||||
const getMaterialHistory = () => { try { return JSON.parse(localStorage.getItem(HISTORY_KEYS.MATERIAL) || '[]') } catch (e) { return [] } }
|
||||
|
||||
// 保存历史 (String 类型)
|
||||
const saveToHistory = (key: string, value: string) => {
|
||||
if (!value) return
|
||||
try {
|
||||
const existing = localStorage.getItem(key)
|
||||
let list = existing ? JSON.parse(existing) : []
|
||||
list = list.filter((i: string) => i !== value)
|
||||
list.unshift(value)
|
||||
if (list.length > 20) list = list.slice(0, 20)
|
||||
localStorage.setItem(key, JSON.stringify(list))
|
||||
} catch (e) { console.error('save history failed', e) }
|
||||
}
|
||||
|
||||
// 获取历史 (String 类型)
|
||||
const getHistoryList = (key: string): any[] => {
|
||||
try {
|
||||
const existing = localStorage.getItem(key)
|
||||
const list = existing ? JSON.parse(existing) : []
|
||||
return list.map((v: string) => ({ value: v }))
|
||||
} catch (e) { return [] }
|
||||
}
|
||||
|
||||
// 保存物料历史 (Object 类型)
|
||||
const saveMaterialHistory = (item: any) => {
|
||||
if (!item || !item.id) return
|
||||
const key = HISTORY_KEYS.MATERIAL
|
||||
try {
|
||||
const existing = localStorage.getItem(key)
|
||||
let list = existing ? JSON.parse(existing) : []
|
||||
list = list.filter((i: any) => i.id !== item.id)
|
||||
list.unshift({ ...item, isHistory: true })
|
||||
if (list.length > 10) list = list.slice(0, 10)
|
||||
localStorage.setItem(key, JSON.stringify(list))
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const getMaterialHistory = () => {
|
||||
try {
|
||||
const existing = localStorage.getItem(HISTORY_KEYS.MATERIAL)
|
||||
return existing ? JSON.parse(existing) : []
|
||||
} catch (e) { return [] }
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Autocomplete 建议逻辑 (混合模式)
|
||||
// ------------------------------------
|
||||
const createFilter = (queryString: string) => {
|
||||
return (item: any) => {
|
||||
return (item.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
|
||||
}
|
||||
}
|
||||
|
||||
const getTableDataUnique = (field: string) => {
|
||||
const uniqueItems = Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean)))
|
||||
return uniqueItems.map(i => ({ value: i }))
|
||||
}
|
||||
|
||||
const mixedSearch = (queryString: string, tableField: string, storageKey: string, cb: any) => {
|
||||
const tableList = getTableDataUnique(tableField)
|
||||
const historyList = getHistoryList(storageKey)
|
||||
const map = new Map()
|
||||
historyList.forEach(i => map.set(i.value, i))
|
||||
tableList.forEach(i => map.set(i.value, i))
|
||||
const allList = Array.from(map.values())
|
||||
const results = queryString ? allList.filter(createFilter(queryString)) : allList
|
||||
cb(results)
|
||||
}
|
||||
|
||||
// 1. 负责人
|
||||
const createFilter = (qs: string) => { return (item: any) => (item.value.toLowerCase().indexOf(qs.toLowerCase()) === 0) }
|
||||
const getTableDataUnique = (field: string) => { return Array.from(new Set(tableData.value.map((i: any) => i[field]).filter(Boolean))).map(i => ({ value: i })) }
|
||||
const mixedSearch = (qs: string, tableField: string, storageKey: string, cb: any) => { const tableList = getTableDataUnique(tableField); const historyList = getHistoryList(storageKey); const map = new Map(); historyList.forEach(i => map.set(i.value, i)); tableList.forEach(i => map.set(i.value, i)); const allList = Array.from(map.values()); const results = qs ? allList.filter(createFilter(qs)) : allList; cb(results) }
|
||||
const querySearchManager = (qs: string, cb: any) => mixedSearch(qs, 'production_manager', HISTORY_KEYS.PRODUCTION_MANAGER, cb)
|
||||
const handleManagerSelect = (item: any) => saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, item.value)
|
||||
|
||||
const fetchData = async () => { loading.value = true; try { const res: any = await getProductList(queryParams); tableData.value = res.data.items || []; total.value = res.data.total || 0 } finally { loading.value = false } }
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getProductList(queryParams)
|
||||
tableData.value = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 物料搜索逻辑 (优化)
|
||||
// ------------------------------------
|
||||
const handleMaterialDropdownVisible = (visible: boolean) => {
|
||||
if (visible) {
|
||||
if (materialOptions.value.length === 0) {
|
||||
handleSearchMaterial('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchMaterial = async (query: string) => {
|
||||
searchLoading.value = true
|
||||
try {
|
||||
const res: any = await searchMaterialBase(query)
|
||||
const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false }))
|
||||
if (!query) {
|
||||
const history = getMaterialHistory()
|
||||
const historyIds = new Set(history.map((h: any) => h.id))
|
||||
const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id))
|
||||
materialOptions.value = [...history, ...filteredApi]
|
||||
} else {
|
||||
materialOptions.value = apiResults
|
||||
}
|
||||
} finally { searchLoading.value = false }
|
||||
}
|
||||
|
||||
const onMaterialSelected = (val: number) => {
|
||||
const item = materialOptions.value.find(i => i.id === val)
|
||||
if (item) {
|
||||
saveMaterialHistory(item)
|
||||
form.material_name = item.name
|
||||
form.spec_model = item.spec
|
||||
form.material_type = item.type
|
||||
form.category = item.category
|
||||
}
|
||||
}
|
||||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterial('') }
|
||||
const handleSearchMaterial = async (query: string) => { searchLoading.value = true; try { const res: any = await searchMaterialBase(query); const apiResults = (res.data || []).map((i: any) => ({ ...i, isHistory: false })); if (!query) { const history = getMaterialHistory(); const historyIds = new Set(history.map((h: any) => h.id)); const filteredApi = apiResults.filter((apiItem: any) => !historyIds.has(apiItem.id)); materialOptions.value = [...history, ...filteredApi] } else { materialOptions.value = apiResults } } finally { searchLoading.value = false } }
|
||||
const onMaterialSelected = (val: number) => { const item = materialOptions.value.find(i => i.id === val); if (item) { saveMaterialHistory(item); form.material_name = item.name; form.spec_model = item.spec; form.material_type = item.type; form.category = item.category } }
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogStatus.value = 'create'
|
||||
@ -439,124 +481,146 @@ const handleCreate = () => {
|
||||
materialOptions.value = []
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// 核心更新逻辑 (回显三个图片字段)
|
||||
// ------------------------------------
|
||||
const handleUpdate = (row: any) => {
|
||||
dialogStatus.value = 'update'
|
||||
Object.assign(form, row)
|
||||
// 转换时间格式
|
||||
if(row.production_start_time && row.production_end_time) {
|
||||
form.production_time_range = [row.production_start_time, row.production_end_time]
|
||||
} else {
|
||||
form.production_time_range = []
|
||||
}
|
||||
// 编辑模式下填充当前物料
|
||||
materialOptions.value = [{
|
||||
id: row.base_id,
|
||||
name: row.material_name,
|
||||
spec: row.spec_model,
|
||||
category: row.category,
|
||||
isHistory: false
|
||||
}]
|
||||
Object.assign(form, {
|
||||
...row,
|
||||
product_photo: row.product_photo || [],
|
||||
quality_report_link: row.quality_report_link || [],
|
||||
inspection_report_link: row.inspection_report_link || [],
|
||||
in_quantity: Number(row.qty_inbound),
|
||||
raw_material_cost: Number(row.raw_material_cost),
|
||||
manual_cost: Number(row.manual_cost),
|
||||
sale_price: Number(row.sale_price)
|
||||
})
|
||||
if(row.production_start_time && row.production_end_time) { form.production_time_range = [row.production_start_time, row.production_end_time] } else { form.production_time_range = [] }
|
||||
|
||||
// 1. 成品实拍
|
||||
productPhotoList.value = form.product_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||
|
||||
// 2. 质量报告
|
||||
const qReports = form.quality_report_link || []
|
||||
qualityFileList.value = qReports.filter(r => !isExternalLink(r)).map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||
const qLinks = qReports.filter(r => isExternalLink(r))
|
||||
quality_url.value = qLinks.length > 0 ? qLinks[0] : ''
|
||||
|
||||
// 3. 检测报告
|
||||
const iReports = form.inspection_report_link || []
|
||||
inspectionFileList.value = iReports.filter(r => !isExternalLink(r)).map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||||
const iLinks = iReports.filter(r => isExternalLink(r))
|
||||
inspection_url.value = iLinks.length > 0 ? iLinks[0] : ''
|
||||
|
||||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, isHistory: false }]
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const getImageUrl = (url: string) => { if (!url) return ''; if (url.startsWith('http')) return url; return url }
|
||||
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
|
||||
const getImagesOnly = (list: string[]) => { if (!list) return []; return list.filter(item => !isExternalLink(item)) }
|
||||
const hasExternalLink = (list: string[]) => { if (!list) return false; return list.some(item => isExternalLink(item)) }
|
||||
const beforeAvatarUpload = (rawFile: any) => { if (rawFile.type !== 'image/jpeg' && rawFile.type !== 'image/png') { ElMessage.error('仅支持 JPG/PNG'); return false } if (rawFile.size / 1024 / 1024 > 5) { ElMessage.error('图片不能超过 5MB'); return false } return true }
|
||||
|
||||
const customUpload = async (options: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
||||
const { file, onSuccess, onError } = options
|
||||
const formData = new FormData(); formData.append('file', file)
|
||||
try {
|
||||
const res: any = await uploadFile(formData)
|
||||
if (res.code === 200) {
|
||||
const newUrl = res.data.url
|
||||
form[targetField].push(newUrl)
|
||||
ElMessage.success('上传成功')
|
||||
onSuccess(res)
|
||||
} else { ElMessage.error(res.msg || '上传失败'); onError(new Error(res.msg)) }
|
||||
} catch (e) { ElMessage.error('网络错误'); onError(e) }
|
||||
}
|
||||
|
||||
const handleRemoveImage = async (uploadFile: any, targetField: 'product_photo' | 'quality_report_link' | 'inspection_report_link') => {
|
||||
try {
|
||||
const urlToRemove = form[targetField].find(u => getImageUrl(u) === uploadFile.url) || uploadFile.url
|
||||
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
||||
if (!isExternalLink(urlToRemove)) { const filename = urlToRemove.split('/').pop(); if (filename) await deleteFile(filename) }
|
||||
ElMessage.success('已删除')
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
const triggerCamera = (field: any) => { currentCameraField.value = field; if (cameraInputRef.value) cameraInputRef.value.click() }
|
||||
const handleCameraFile = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement; if (input.files && input.files[0]) {
|
||||
const file = input.files[0]; if (!beforeAvatarUpload(file)) { input.value = ''; return }
|
||||
const formData = new FormData(); formData.append('file', file); const loadingMsg = ElMessage.loading({ message: '上传中...', duration: 0 })
|
||||
try {
|
||||
const res: any = await uploadFile(formData)
|
||||
if (res.code === 200) {
|
||||
const newUrl = res.data.url; const field = currentCameraField.value; form[field].push(newUrl)
|
||||
if (field === 'product_photo') productPhotoList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
||||
else if (field === 'quality_report_link') qualityFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
||||
else inspectionFileList.value.push({ name: newUrl.split('/').pop(), url: getImageUrl(newUrl) })
|
||||
ElMessage.success('拍照上传成功')
|
||||
} else { ElMessage.error(res.msg || '上传失败') }
|
||||
} catch (e) { ElMessage.error('网络错误') } finally { loadingMsg.close(); input.value = '' }
|
||||
}
|
||||
}
|
||||
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
||||
|
||||
// ------------------------------------
|
||||
// 提交逻辑 (含自动打印)
|
||||
// 提交逻辑 (合并链接)
|
||||
// ------------------------------------
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if(valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = { ...form,
|
||||
production_start_time: form.production_time_range?.[0],
|
||||
production_end_time: form.production_time_range?.[1]
|
||||
}
|
||||
|
||||
// 合并 Quality 链接
|
||||
const qList = [...form.quality_report_link]
|
||||
const qImages = qList.filter(item => !isExternalLink(item))
|
||||
if (quality_url.value && !qList.includes(quality_url.value)) qImages.push(quality_url.value)
|
||||
else if (quality_url.value) qImages.push(quality_url.value) // 重新添加输入框内容
|
||||
|
||||
// 合并 Inspection 链接
|
||||
const iList = [...form.inspection_report_link]
|
||||
const iImages = iList.filter(item => !isExternalLink(item))
|
||||
if (inspection_url.value && !iList.includes(inspection_url.value)) iImages.push(inspection_url.value)
|
||||
else if (inspection_url.value) iImages.push(inspection_url.value)
|
||||
|
||||
const payload = { ...form,
|
||||
quality_report_link: qImages,
|
||||
inspection_report_link: iImages,
|
||||
production_start_time: form.production_time_range?.[0],
|
||||
production_end_time: form.production_time_range?.[1]
|
||||
}
|
||||
delete payload.production_time_range
|
||||
|
||||
try {
|
||||
if(dialogStatus.value === 'create') {
|
||||
// 1. 创建入库
|
||||
const res: any = await createProductInbound(payload)
|
||||
ElMessage.success('入库成功')
|
||||
|
||||
// 2. 自动打印
|
||||
const newItem = res.data
|
||||
if (newItem) {
|
||||
ElMessage.info('正在发送打印指令...')
|
||||
try {
|
||||
await executePrint(newItem)
|
||||
ElMessage.success('打印指令已发送')
|
||||
} catch (printErr: any) {
|
||||
console.error(printErr)
|
||||
ElMessage.warning('入库成功,但自动打印失败:' + (printErr.msg || '未知错误'))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (newItem) { ElMessage.info('发送打印...'); try { await executePrint(newItem); ElMessage.success('指令已发送') } catch (e: any) { ElMessage.warning('打印失败') } }
|
||||
} else {
|
||||
await updateProductInbound(form.id!, payload)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
|
||||
// 保存历史
|
||||
saveToHistory(HISTORY_KEYS.PRODUCTION_MANAGER, form.production_manager)
|
||||
|
||||
visible.value = false
|
||||
fetchData()
|
||||
} catch(e:any) { ElMessage.error(e.msg || '失败') }
|
||||
finally { submitting.value = false }
|
||||
visible.value = false; fetchData()
|
||||
} catch(e:any) { ElMessage.error(e.msg || '失败') } finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try { await deleteProductInbound(row.id); ElMessage.success('删除成功'); fetchData() }
|
||||
catch(e) { ElMessage.error('删除失败') }
|
||||
}
|
||||
const handleDelete = async (row: any) => { try { await deleteProductInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch(e) { ElMessage.error('删除失败') } }
|
||||
|
||||
// ------------------------------------
|
||||
// 打印逻辑 (手动 & 预览)
|
||||
// ------------------------------------
|
||||
const handlePrint = async (row: any) => {
|
||||
printVisible.value = true
|
||||
printLoading.value = true
|
||||
previewUrl.value = ''
|
||||
|
||||
// 构造产品特有的打印数据
|
||||
const printData = {
|
||||
global_print_id: row.global_print_id, // 需后端模型支持
|
||||
material_name: row.material_name,
|
||||
spec_model: row.spec_model,
|
||||
category: row.category,
|
||||
material_type: row.material_type,
|
||||
warehouse_loc: row.warehouse_loc,
|
||||
serial_number: row.serial_number,
|
||||
sku: row.sku
|
||||
}
|
||||
currentPrintData.value = printData
|
||||
|
||||
try {
|
||||
const res: any = await getLabelPreview(printData)
|
||||
previewUrl.value = res.data
|
||||
} catch (e) {
|
||||
ElMessage.error('预览生成失败')
|
||||
} finally {
|
||||
printLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmPrint = async () => {
|
||||
printing.value = true
|
||||
try {
|
||||
await executePrint(currentPrintData.value)
|
||||
ElMessage.success('指令已发送')
|
||||
printVisible.value = false
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.msg || '打印失败')
|
||||
} finally {
|
||||
printing.value = false
|
||||
}
|
||||
printVisible.value = true; printLoading.value = true; previewUrl.value = ''
|
||||
currentPrintData.value = { global_print_id: row.global_print_id, material_name: row.material_name, spec_model: row.spec_model, category: row.category, material_type: row.material_type, warehouse_loc: row.warehouse_loc, serial_number: row.serial_number, sku: row.sku }
|
||||
try { const res: any = await getLabelPreview(currentPrintData.value); previewUrl.value = res.data } catch (e) { ElMessage.error('预览失败') } finally { printLoading.value = false }
|
||||
}
|
||||
const confirmPrint = async () => { printing.value = true; try { await executePrint(currentPrintData.value); ElMessage.success('已发送'); printVisible.value = false } catch (e: any) { ElMessage.error('打印失败') } finally { printing.value = false } }
|
||||
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []
|
||||
materialOptions.value = []; productPhotoList.value = []; qualityFileList.value = []; inspectionFileList.value = []; quality_url.value = ''; inspection_url.value = ''
|
||||
Object.assign(form, {
|
||||
id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '', category: '',
|
||||
sku: '', barcode: '', serial_number: '', in_date: '',
|
||||
@ -565,7 +629,7 @@ const resetForm = () => {
|
||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
||||
production_manager: '', production_time_range: [],
|
||||
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
||||
quality_report_link: '', inspection_report_link: '', detail_link: ''
|
||||
quality_report_link: [], inspection_report_link: [], product_photo: [], detail_link: ''
|
||||
})
|
||||
}
|
||||
|
||||
@ -577,34 +641,39 @@ onMounted(() => fetchData())
|
||||
|
||||
<style scoped>
|
||||
.product-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||||
.header-tools { display: flex; justify-content: space-between; margin-bottom: 20px; background: #fff; padding: 15px; border-radius: 8px; }
|
||||
.header-tools { display: flex; justify-content: space-between; margin-bottom: 20px; background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
.modern-table { border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; }
|
||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
||||
.stock-num { font-weight: bold; font-size: 15px; }
|
||||
.sum-tag { margin-left: 4px; transform: scale(0.9); }
|
||||
.form-card { background: #fff; border-radius: 8px; margin-bottom: 20px; border: 1px solid #e4e7ed; overflow: hidden; }
|
||||
.card-title { background: #fcfcfc; padding: 10px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
||||
.card-title .icon { font-size: 18px; }
|
||||
.card-content { padding: 20px; }
|
||||
.basic-card { border-left: 4px solid #409EFF; }
|
||||
.inbound-card { border-left: 4px solid #67C23A; }
|
||||
.production-card { border-left: 4px solid #E6A23C; }
|
||||
.basic-card { border-left: 4px solid #409EFF; } .basic-card .icon { color: #409EFF; }
|
||||
.inbound-card { border-left: 4px solid #67C23A; } .inbound-card .icon { color: #67C23A; }
|
||||
.production-card { border-left: 4px solid #E6A23C; } .production-card .icon { color: #E6A23C; }
|
||||
.identity-panel { background: #fffbf0; border: 1px dashed #e6a23c; padding: 15px; margin: 10px 0; border-radius: 6px; }
|
||||
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; padding: 0 5px; font-weight: bold; }
|
||||
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; padding: 0 5px; font-weight: bold; border-radius: 4px; }
|
||||
.option-item { display: flex; justify-content: space-between; width: 100%; align-items: center; }
|
||||
.opt-name { font-weight: bold; }
|
||||
.opt-spec { color: #8492a6; font-size: 12px; margin-right: 10px; }
|
||||
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background: #f5f7fa; border-bottom: 1px solid #dcdfe6; }
|
||||
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background: #f5f7fa; border-bottom: 1px solid #dcdfe6; padding-left: 0; }
|
||||
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
|
||||
.dialog-scroll-container { padding: 20px; max-height: 70vh; overflow-y: auto; }
|
||||
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; padding: 20px; border-top: 1px solid #ebeef5; }
|
||||
.money-text { font-family: 'Consolas', monospace; color: #303133; }
|
||||
.preview-box { min-height: 150px; display: flex; justify-content: center; align-items: center; background: #f5f7fa; border-radius: 4px; }
|
||||
.empty-preview { color: #909399; }
|
||||
.more-images-badge { margin-left: 5px; background: #909399; color: #fff; border-radius: 10px; padding: 0 6px; font-size: 12px; }
|
||||
.clickable-text { color: #409EFF; cursor: pointer; font-weight: 500; text-decoration: underline; }
|
||||
|
||||
/* 打印预览样式 */
|
||||
.preview-box {
|
||||
min-height: 150px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.empty-preview {
|
||||
color: #909399;
|
||||
}
|
||||
.upload-container { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
:deep(.el-upload--picture-card) { width: 100px; height: 100px; line-height: 100px; }
|
||||
:deep(.el-upload-list--picture-card .el-upload-list__item) { width: 100px; height: 100px; }
|
||||
.camera-card { width: 100px; height: 100px; background-color: #fbfdff; border: 1px dashed #c0ccda; border-radius: 6px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer; transition: all 0.3s; color: #8c939d; }
|
||||
.camera-card:hover { border-color: #409EFF; color: #409EFF; }
|
||||
.camera-card .text { font-size: 12px; margin-top: 5px; }
|
||||
.camera-card .el-icon { font-size: 24px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user