1784 lines
75 KiB
Vue
1784 lines
75 KiB
Vue
<template>
|
||
<div class="buy-module">
|
||
<div class="header-container" style="flex-wrap: wrap;">
|
||
<div class="search-form-area" style="flex-wrap: wrap;">
|
||
|
||
<el-select
|
||
v-model="queryParams.company"
|
||
placeholder="所属公司"
|
||
class="filter-item-select"
|
||
clearable
|
||
filterable
|
||
@change="fetchData"
|
||
style="width: 160px;"
|
||
>
|
||
<el-option v-for="item in companyOptions" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
|
||
<el-input
|
||
v-model="queryParams.keyword"
|
||
placeholder="请输入搜索关键字"
|
||
class="filter-item-input"
|
||
clearable
|
||
@clear="fetchData"
|
||
@keyup.enter="fetchData"
|
||
style="width: 280px;"
|
||
>
|
||
<template #prepend>
|
||
<el-select v-model="queryParams.searchField" style="width: 90px">
|
||
<el-option label="全部" value="all" />
|
||
<el-option label="名称" value="name" />
|
||
<el-option label="规格" value="spec" />
|
||
<el-option label="条码" value="barcode" />
|
||
<el-option label="批号" value="batch_number" />
|
||
</el-select>
|
||
</template>
|
||
</el-input>
|
||
|
||
<el-input
|
||
v-model="queryParams.sku"
|
||
placeholder="请输入SKU搜索..."
|
||
class="filter-item-input"
|
||
clearable
|
||
@input="handleInputSearch"
|
||
@clear="fetchData"
|
||
@keyup.enter="fetchData"
|
||
style="width: 160px;"
|
||
>
|
||
<template #prefix><el-icon><Search /></el-icon></template>
|
||
</el-input>
|
||
|
||
<el-select
|
||
v-model="queryParams.category"
|
||
placeholder="类别"
|
||
class="filter-item-select"
|
||
clearable
|
||
filterable
|
||
@change="fetchData"
|
||
style="width: 160px;"
|
||
>
|
||
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
|
||
<el-select
|
||
v-model="queryParams.material_type"
|
||
placeholder="类型"
|
||
class="filter-item-select"
|
||
clearable
|
||
filterable
|
||
@change="fetchData"
|
||
style="width: 160px;"
|
||
>
|
||
<el-option v-for="item in typeOptions" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
|
||
<el-button type="primary" plain class="search-btn" @click="fetchData">搜索</el-button>
|
||
<el-button class="reset-btn" @click="resetQuery">重置</el-button>
|
||
<el-popover
|
||
v-model:visible="advancedFilterVisible"
|
||
placement="bottom"
|
||
title="高级筛选"
|
||
width="600"
|
||
trigger="manual">
|
||
<template #reference>
|
||
<el-button plain @click="advancedFilterVisible = !advancedFilterVisible">高级筛选</el-button>
|
||
</template>
|
||
<div class="advanced-filter">
|
||
<div v-for="(condition, index) in advancedConditions" :key="index" class="condition-row" style="display: flex; align-items: center; margin-bottom: 10px;">
|
||
<el-select v-model="condition.field" placeholder="字段" style="width: 180px">
|
||
<el-option v-for="field in fieldOptions" :key="field.value" :label="field.label" :value="field.value" />
|
||
</el-select>
|
||
<el-select v-model="condition.operator" placeholder="操作符" style="width: 120px; margin-left: 8px">
|
||
<el-option v-for="op in operatorOptions" :key="op.value" :label="op.label" :value="op.value" />
|
||
</el-select>
|
||
<el-input v-model="condition.value" placeholder="值" style="width: 180px; margin-left: 8px" />
|
||
<el-button v-if="advancedConditions.length > 1" type="danger" link @click="removeCondition(index)" style="margin-left: 8px">删除</el-button>
|
||
</div>
|
||
<div style="margin-top: 12px">
|
||
<el-button type="primary" link @click="addCondition">添加条件</el-button>
|
||
<el-button @click="applyAdvancedFilter" type="primary">应用筛选</el-button>
|
||
<el-button @click="resetAdvancedFilter">重置</el-button>
|
||
</div>
|
||
</div>
|
||
</el-popover>
|
||
</div>
|
||
|
||
<div class="right-actions" style="flex-wrap: wrap;">
|
||
<el-button v-if="userStore.hasPermission('inbound_buy:operation')" type="primary" :icon="Plus" @click="handleCreate" class="add-btn">新增</el-button>
|
||
<el-button :icon="Refresh" circle @click="fetchData" class="circle-btn" />
|
||
|
||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||
<template #reference>
|
||
<el-button :icon="Setting" circle class="circle-btn" />
|
||
</template>
|
||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||
<div class="col-group-title">基础信息</div>
|
||
<el-row :gutter="10">
|
||
<el-col :span="12" v-for="c in baseColumns" :key="c.prop">
|
||
<el-checkbox :label="c.prop">{{ c.label }}</el-checkbox>
|
||
</el-col>
|
||
</el-row>
|
||
<div class="col-group-title" style="margin-top:10px">库存与商务</div>
|
||
<el-row :gutter="10">
|
||
<el-col :span="12" v-for="c in stockColumns" :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"
|
||
highlight-current-row
|
||
header-cell-class-name="table-header-gray"
|
||
@sort-change="handleSortChange"
|
||
>
|
||
<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 || '140'"
|
||
show-overflow-tooltip
|
||
:sortable="isColumnSortable(col.prop) ? 'custom' : false"
|
||
>
|
||
<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="col.prop === 'sn_bn'">
|
||
<div v-if="scope.row.serial_number" class="id-cell">
|
||
<span class="prefix-tag sn">SN</span>
|
||
<span class="id-text">{{ scope.row.serial_number }}</span>
|
||
</div>
|
||
<div v-else-if="scope.row.batch_number" class="id-cell">
|
||
<span class="prefix-tag bn">BN</span>
|
||
<span class="id-text">{{ scope.row.batch_number }}</span>
|
||
</div>
|
||
<span v-else class="text-placeholder">-</span>
|
||
</template>
|
||
|
||
<template #default="scope" v-else-if="col.prop === 'status'">
|
||
<el-tag :type="getStatusType(scope.row.status)" effect="light" round>
|
||
{{ scope.row.status }}
|
||
</el-tag>
|
||
</template>
|
||
|
||
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
|
||
<span class="stock-num">{{ scope.row.qty_stock }}</span>
|
||
</template>
|
||
<template #default="scope" v-else-if="col.prop === 'qty_available'">
|
||
<span class="avail-num">{{ scope.row.qty_available }}</span>
|
||
</template>
|
||
|
||
<template #default="scope" v-else-if="col.prop === 'tax_rate'">
|
||
<span style="color: #909399;">{{ scope.row.tax_rate }}%</span>
|
||
</template>
|
||
|
||
<template #default="scope" v-else-if="['arrival_photo', 'inspection_report'].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"
|
||
lazy
|
||
>
|
||
<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="col.prop.includes('link')">
|
||
<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>
|
||
</template>
|
||
|
||
<template #default="scope" v-else-if="['unit_price', 'total_price'].includes(col.prop)">
|
||
<span class="money-text">{{ formatMoney(scope.row[col.prop], scope.row.currency) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
</template>
|
||
|
||
<el-table-column v-if="userStore.hasPermission('inbound_buy:operation')" label="操作" width="220" fixed="right" align="center">
|
||
<template #default="{ row }">
|
||
<el-button link type="warning" size="default" @click="handlePrint(row)">
|
||
<el-icon><Printer/></el-icon> 打印
|
||
</el-button>
|
||
<el-button link type="primary" size="default" @click="handleUpdate(row)">编辑</el-button>
|
||
<el-popconfirm title="确定删除该条记录吗?不可恢复。" @confirm="handleDelete(row)" width="220">
|
||
<template #reference>
|
||
<el-button link type="danger" size="default" v-permission="'inbound_buy:delete'">删除</el-button>
|
||
</template>
|
||
</el-popconfirm>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<el-pagination
|
||
class="pagination-bar"
|
||
v-model:current-page="queryParams.page"
|
||
v-model:page-size="queryParams.pageSize"
|
||
:total="total"
|
||
:page-sizes="[100, 200, 500, 1000]"
|
||
layout="total, sizes, prev, pager, next, jumper"
|
||
background
|
||
@size-change="fetchData"
|
||
@current-change="fetchData"
|
||
/>
|
||
|
||
<el-dialog
|
||
v-model="visible"
|
||
:title="dialogStatus === 'create' ? '新增采购入库' : '编辑入库信息'"
|
||
:width="'min(1000px, 95vw)'"
|
||
top="4vh"
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
class="stylish-dialog compact-layout"
|
||
>
|
||
<div class="dialog-scroll-container">
|
||
<el-form :model="form" label-width="100px" ref="formRef" :rules="rules" size="default" class="stylish-form">
|
||
|
||
<div class="form-card basic-card">
|
||
<div class="card-title">
|
||
<div style="display: flex; align-items: center;">
|
||
<el-icon class="icon"><Box/></el-icon>
|
||
<span>1. 基础信息</span>
|
||
</div>
|
||
<span class="sub-title" v-if="dialogStatus === 'create'"> (请先搜索锁定物料)</span>
|
||
</div>
|
||
<div class="card-content">
|
||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||
<el-col :span="12">
|
||
<el-form-item label="物料搜索" prop="base_id" class="highlight-label">
|
||
<el-select
|
||
v-model="form.base_id"
|
||
filterable
|
||
remote
|
||
reserve-keyword
|
||
clearable
|
||
placeholder="请输入名称或规格进行检索..."
|
||
:remote-method="handleSearchMaterialDebounced"
|
||
@visible-change="handleMaterialDropdownVisible"
|
||
:loading="searchLoading"
|
||
style="width: 100%"
|
||
@change="onMaterialSelected"
|
||
default-first-option
|
||
v-loadmore="loadMoreMaterials"
|
||
popper-class="long-dropdown"
|
||
>
|
||
<template #prefix>
|
||
<el-icon><Search /></el-icon>
|
||
</template>
|
||
<el-option
|
||
v-for="item in materialOptions"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
>
|
||
<div class="option-item">
|
||
<div class="opt-main">
|
||
<span class="opt-name" :title="item.name">{{ item.name }}</span>
|
||
</div>
|
||
<div class="opt-meta">
|
||
<span class="opt-spec" :title="item.spec">{{ item.spec || '-' }}</span>
|
||
</div>
|
||
<div class="opt-tags">
|
||
<el-tag size="small" type="info" effect="light" class="company-tag">{{ item.company_name }}</el-tag>
|
||
<el-tag v-if="item.isHistory" size="small" type="warning" effect="plain">历史</el-tag>
|
||
<el-tag v-else size="small" type="success" effect="plain">系统</el-tag>
|
||
</div>
|
||
</div>
|
||
</el-option>
|
||
<div v-if="loadingMore" style="text-align: center; color: #999; font-size: 12px; padding: 8px; background: #f9f9f9;">
|
||
<el-icon class="is-loading"><Refresh /></el-icon> 加载更多中...
|
||
</div>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12" 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="20">
|
||
<el-col :span="8"><el-form-item label="所属公司" v-if="hasFormFieldPermission('company_name')"><el-input v-model="form.company_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||
<el-col :span="8"><el-form-item label="名称" v-if="hasFormFieldPermission('material_name')"><el-input v-model="form.material_name" readonly class="is-text-view"/></el-form-item></el-col>
|
||
<el-col :span="8"><el-form-item label="类型" v-if="hasFormFieldPermission('material_type')"><el-input v-model="form.material_type" readonly class="is-text-view"/></el-form-item></el-col>
|
||
<el-col :span="8"><el-form-item label="类别" v-if="hasFormFieldPermission('category')"><el-input v-model="form.category" readonly class="is-text-view"/></el-form-item></el-col>
|
||
<el-col :span="8"><el-form-item label="规格型号" v-if="hasFormFieldPermission('spec_model')"><el-input v-model="form.spec_model" readonly class="is-text-view"/></el-form-item></el-col>
|
||
<el-col :span="8"><el-form-item label="单位" v-if="hasFormFieldPermission('unit')"><el-input v-model="form.unit" readonly 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="20">
|
||
<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="in_date"><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-col :span="6">
|
||
<el-form-item label="库位" prop="warehouse_location">
|
||
<WarehouseSelector
|
||
v-model="form.warehouse_location"
|
||
:options="warehouseOptions"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<div class="identity-panel">
|
||
<el-row>
|
||
<el-col :span="24" style="margin-bottom: 8px;">
|
||
<el-radio-group v-model="entryMode" @change="handleEntryModeChange" :disabled="modeLocked" size="small" class="custom-radio-group">
|
||
<el-radio-button label="batch">按批号入库 (Batch)</el-radio-button>
|
||
<el-radio-button label="serial">按序列号入库 (SN)</el-radio-button>
|
||
</el-radio-group>
|
||
<span v-if="modeLocked" class="locked-msg"><el-icon><Lock/></el-icon> 历史锁定 (同物料遵循历史模式)</span>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="批号" prop="batch_number">
|
||
<el-input v-model="form.batch_number" :placeholder="entryMode === 'batch' ? '系统生成...' : '不可用'" :disabled="entryMode === 'serial'" clearable>
|
||
<template #prefix><span class="prefix-tag bn">BN</span></template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="序列号" prop="serial_number">
|
||
<el-input v-model="form.serial_number" :placeholder="entryMode === 'serial' ? '请扫描 SN...' : '不可用'" :disabled="entryMode === 'batch'" clearable>
|
||
<template #prefix><span class="prefix-tag sn">SN</span></template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<el-row :gutter="20" style="margin-top: 10px;">
|
||
<el-col :span="6">
|
||
<el-form-item label="入库数量" prop="in_quantity">
|
||
<el-input-number v-model="form.in_quantity" :min="1" controls-position="right" style="width:100%" class="strong-input" @change="updatePrices('qty')"/>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="6" v-if="dialogStatus === 'create'">
|
||
<el-form-item label="打印份数" prop="print_copies">
|
||
<el-input-number v-model="form.print_copies" :min="1" :max="999" controls-position="right" style="width:100%"/>
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<template v-if="dialogStatus === 'update'">
|
||
<el-col :span="6"><el-form-item label="当前库存" prop="stock_quantity"><el-input-number v-model="form.stock_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||
<el-col :span="6"><el-form-item label="当前可用" prop="available_quantity"><el-input-number v-model="form.available_quantity" disabled style="width:100%" :controls="false"/></el-form-item></el-col>
|
||
<el-col :span="6">
|
||
<el-form-item label="库存状态" prop="status">
|
||
<el-select v-model="form.status" style="width:100%">
|
||
<el-option label="在库" value="在库"/>
|
||
<el-option label="已出库" value="已出库"/>
|
||
<el-option label="借库" value="借库"/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</template>
|
||
<el-col :span="6">
|
||
<el-form-item label="到检状态" prop="inspection_status">
|
||
<el-select v-model="form.inspection_status" style="width:100%">
|
||
<el-option label="未检" value="未检"><span style="color:#909399">⚪ 未检</span></el-option>
|
||
<el-option label="合格" value="合格"><span style="color:#67C23A">🟢 合格</span></el-option>
|
||
<el-option label="不合格" value="不合格"><span style="color:#F56C6C">🔴 不合格</span></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="到货图片" prop="arrival_photo">
|
||
<div class="upload-container">
|
||
<el-upload
|
||
v-model:file-list="arrivalFileList"
|
||
action="#"
|
||
list-type="picture-card"
|
||
multiple
|
||
:http-request="(opts) => customUpload(opts, 'arrival_photo')"
|
||
:on-preview="handlePreviewPicture"
|
||
:on-remove="(file) => handleRemoveImage(file, 'arrival_photo')"
|
||
:before-upload="beforeAvatarUpload">
|
||
<el-icon><Plus /></el-icon>
|
||
</el-upload>
|
||
<div class="camera-card" @click="triggerCamera('arrival_photo')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||
</div>
|
||
<el-input v-model="form.arrival_photo" placeholder="图片列表" style="display:none;" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="检测报告" prop="inspection_report">
|
||
<div class="upload-container">
|
||
<el-upload
|
||
v-model:file-list="reportFileList"
|
||
action="#"
|
||
list-type="picture-card"
|
||
multiple
|
||
:http-request="(opts) => customUpload(opts, 'inspection_report')"
|
||
:on-preview="handlePreviewPicture"
|
||
:on-remove="(file) => handleRemoveImage(file, 'inspection_report')"
|
||
:before-upload="beforeAvatarUpload">
|
||
<el-icon><Plus /></el-icon>
|
||
</el-upload>
|
||
<div class="camera-card" @click="triggerCamera('inspection_report')"><el-icon><Camera /></el-icon><span class="text">拍照</span></div>
|
||
</div>
|
||
<el-input v-model="inspection_report_url" placeholder="如有外部报告链接,请在此输入 (选填)" style="margin-top: 8px;" clearable><template #prefix><el-icon><Link /></el-icon></template></el-input>
|
||
<el-input v-model="form.inspection_report" placeholder="图片列表" style="display:none;" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<div class="divider-text">商务与采购信息</div>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :span="8">
|
||
<el-form-item label="币种">
|
||
<el-autocomplete v-model="form.currency" :fetch-suggestions="querySearchCurrency" placeholder="币种" style="width: 100%" :trigger-on-focus="true">
|
||
<template #default="{ item }"><span>{{ item.value }}</span><span style="float:right; color:#999; font-size:12px">{{ item.desc }}</span></template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="汇率">
|
||
<el-input-number v-model="form.exchange_rate" :precision="2" controls-position="right" style="width:100%"/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="税率">
|
||
<el-select v-model="form.tax_rate" style="width:100%" @change="updatePrices('tax')">
|
||
<el-option label="0%" :value="0" />
|
||
<el-option label="1%" :value="1" />
|
||
<el-option label="13%" :value="13" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20" style="margin-top: 15px;">
|
||
<el-col :span="8">
|
||
<el-form-item label="不含税单价" prop="unit_price">
|
||
<el-input-number
|
||
v-model="form.unit_price"
|
||
:precision="2"
|
||
:controls="false"
|
||
style="width:100%"
|
||
placeholder="请输入"
|
||
@change="updatePrices('pre')"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="含税单价">
|
||
<el-input-number
|
||
v-model="form.post_tax_unit_price"
|
||
:precision="2"
|
||
:controls="false"
|
||
style="width:100%"
|
||
placeholder="请输入"
|
||
@change="updatePrices('post')"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="不含税总价">
|
||
<el-input-number
|
||
v-model="form.total_price"
|
||
:precision="2"
|
||
disabled
|
||
:controls="false"
|
||
style="width:100%"
|
||
class="total-price-input"
|
||
placeholder="自动计算"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="20" style="margin-top: 15px;">
|
||
<el-col :span="8">
|
||
<el-form-item label="供应商">
|
||
<el-autocomplete
|
||
v-model="form.supplier_name"
|
||
:fetch-suggestions="querySearchSupplier"
|
||
placeholder="输入或选择供应商"
|
||
style="width: 100%"
|
||
clearable
|
||
:trigger-on-focus="true"
|
||
@select="handleSupplierSelect"
|
||
>
|
||
<template #default="{ item }">
|
||
<div style="font-weight: 500">{{ item.value }}</div>
|
||
</template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="采购人">
|
||
<el-autocomplete
|
||
v-model="form.purchaser"
|
||
:fetch-suggestions="querySearchPurchaser"
|
||
placeholder="输入采购人"
|
||
style="width: 100%"
|
||
clearable
|
||
:trigger-on-focus="true"
|
||
@select="handlePurchaserSelect"
|
||
>
|
||
<template #default="{ item }">
|
||
<div style="display: flex; justify-content: space-between;">
|
||
<span style="font-weight: 500">{{ item.value }}</span>
|
||
<span v-if="item.email" style="color: #999; font-size: 12px;">{{ item.email }}</span>
|
||
</div>
|
||
</template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="8">
|
||
<el-form-item label="采购邮箱">
|
||
<el-input v-model="form.purchaser_email" placeholder="自动填充或手动输入" clearable />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="原始链接">
|
||
<el-autocomplete
|
||
v-model="form.source_link"
|
||
:fetch-suggestions="(qs, cb) => querySearchLinks(qs, cb, 'original')"
|
||
placeholder="http://"
|
||
style="width: 100%"
|
||
clearable
|
||
:trigger-on-focus="true"
|
||
>
|
||
<template #default="{ item }"><div style="font-size: 12px; line-height: 1.2; padding: 4px 0;">{{ item.value }}</div></template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="详情链接">
|
||
<el-autocomplete
|
||
v-model="form.detail_link"
|
||
:fetch-suggestions="(qs, cb) => querySearchLinks(qs, cb, 'detail')"
|
||
placeholder="http://"
|
||
style="width: 100%"
|
||
clearable
|
||
:trigger-on-focus="true"
|
||
>
|
||
<template #default="{ item }"><div style="font-size: 12px; line-height: 1.2; padding: 4px 0;">{{ item.value }}</div></template>
|
||
</el-autocomplete>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="visible = false" size="large">取消</el-button>
|
||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large" class="confirm-btn">{{ dialogStatus === 'create' ? '确认入库并打印' : '保存修改' }}</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<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="cameraDialogVisible" title="拍照上传" width="500px" append-to-body destroy-on-close :close-on-click-modal="false">
|
||
<WebRtcCamera
|
||
ref="cameraRef"
|
||
@photo-submit="handleCameraConfirm"
|
||
@cancel="cameraDialogVisible = false"
|
||
/>
|
||
</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>
|
||
|
||
<div style="margin-top: 15px; display: flex; align-items: center; justify-content: center; gap: 10px;">
|
||
<span style="font-weight: bold; color: #303133;">打印份数:</span>
|
||
<el-input-number v-model="printCopies" :min="1" :max="100" size="default" style="width: 120px;" />
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<div class="dialog-footer"><el-button @click="printVisible = false">取消</el-button><el-button type="primary" :loading="printing" @click="confirmPrint"><el-icon><Printer/></el-icon>确认打印</el-button></div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import {ref, reactive, onMounted, watch, computed} from 'vue'
|
||
import {Plus, Setting, Refresh, Search, Lock, Box, House, InfoFilled, Link, Printer, Camera, Delete, Picture} from '@element-plus/icons-vue'
|
||
import {ElMessage, ElMessageBox, ElLoading} from 'element-plus'
|
||
import dayjs from 'dayjs'
|
||
import {
|
||
getBuyList,
|
||
createBuyInbound,
|
||
updateBuyInbound,
|
||
deleteBuyInbound,
|
||
searchMaterialBase,
|
||
uploadFile,
|
||
deleteFile,
|
||
getSupplierSuggestions,
|
||
getUserSuggestions,
|
||
getLinkSuggestions,
|
||
getLocationSuggestions,
|
||
getFilterOptions
|
||
} from '@/api/inbound/buy'
|
||
import {getLabelPreview, executePrint} from '@/api/common/print'
|
||
import { getWarehouseTree } from '@/api/common/warehouse'
|
||
import WebRtcCamera from '@/components/Camera/WebRtcCamera.vue'
|
||
import WarehouseSelector from '@/components/WarehouseSelector.vue'
|
||
import { useUserStore } from '@/stores/user'
|
||
|
||
// ------------------------------------
|
||
// 防抖函数
|
||
// ------------------------------------
|
||
const debounce = (fn: Function, delay: number = 500) => {
|
||
let timer: ReturnType<typeof setTimeout> | null = null
|
||
return (...args: any[]) => {
|
||
if (timer) clearTimeout(timer)
|
||
timer = setTimeout(() => fn(...args), delay)
|
||
}
|
||
}
|
||
|
||
// ------------------------------------
|
||
// 自定义指令:v-loadmore (适配 Teleport 到 Body 的下拉框)
|
||
// ------------------------------------
|
||
const vLoadmore = {
|
||
mounted(el: any, binding: any) {
|
||
const checkAndBind = () => {
|
||
const dropDownWrap = document.querySelector('.long-dropdown .el-select-dropdown__wrap')
|
||
if (dropDownWrap && !dropDownWrap.getAttribute('data-loadmore-bound')) {
|
||
dropDownWrap.setAttribute('data-loadmore-bound', 'true')
|
||
dropDownWrap.addEventListener('scroll', function (this: any) {
|
||
const condition = this.scrollHeight - this.scrollTop <= this.clientHeight + 1
|
||
if (condition) {
|
||
binding.value()
|
||
}
|
||
})
|
||
}
|
||
}
|
||
setTimeout(checkAndBind, 500)
|
||
el.addEventListener('click', () => setTimeout(checkAndBind, 300))
|
||
}
|
||
}
|
||
|
||
// ------------------------------------
|
||
// 表单字段权限检查
|
||
// ------------------------------------
|
||
const hasFormFieldPermission = (fieldName: string) => {
|
||
// 超级管理员直接返回true
|
||
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
|
||
return true
|
||
}
|
||
// 根据字段名映射到权限码
|
||
const map: Record<string, string> = {
|
||
company_name: 'inbound_buy:company_name',
|
||
material_name: 'inbound_buy:material_name',
|
||
spec_model: 'inbound_buy:spec_model',
|
||
category: 'inbound_buy:category',
|
||
material_type: 'inbound_buy:material_type',
|
||
unit: 'inbound_buy:unit',
|
||
sku: 'inbound_buy:sku',
|
||
barcode: 'inbound_buy:barcode',
|
||
in_date: 'inbound_buy:in_date',
|
||
serial_number: 'inbound_buy:serial_number',
|
||
batch_number: 'inbound_buy:batch_number',
|
||
status: 'inbound_buy:status',
|
||
inspection_status: 'inbound_buy:inspection_status',
|
||
in_quantity: 'inbound_buy:in_quantity',
|
||
stock_quantity: 'inbound_buy:stock_quantity',
|
||
available_quantity: 'inbound_buy:available_quantity',
|
||
warehouse_location: 'inbound_buy:warehouse_location',
|
||
unit_price: 'inbound_buy:unit_price',
|
||
tax_rate: 'inbound_buy:tax_rate',
|
||
total_price: 'inbound_buy:total_price',
|
||
currency: 'inbound_buy:currency',
|
||
exchange_rate: 'inbound_buy:exchange_rate',
|
||
supplier_name: 'inbound_buy:supplier_name',
|
||
purchaser: 'inbound_buy:purchaser',
|
||
purchaser_email: 'inbound_buy:purchaser_email',
|
||
source_link: 'inbound_buy:original_link',
|
||
detail_link: 'inbound_buy:detail_link',
|
||
arrival_photo: 'inbound_buy:arrival_photo',
|
||
inspection_report: 'inbound_buy:inspection_report',
|
||
print_copies: 'inbound_buy:print_copies',
|
||
}
|
||
const code = map[fieldName]
|
||
if (!code) {
|
||
// 没有映射的字段默认显示
|
||
return true
|
||
}
|
||
return userStore.hasPermission(code)
|
||
}
|
||
|
||
// ------------------------------------
|
||
// 状态与变量
|
||
// ------------------------------------
|
||
const userStore = useUserStore()
|
||
const loading = ref(false)
|
||
const submitting = ref(false)
|
||
const visible = ref(false)
|
||
const searchLoading = ref(false)
|
||
const loadingMore = ref(false)
|
||
const dialogStatus = ref<'create' | 'update'>('create')
|
||
const tableData = ref([])
|
||
const total = ref(0)
|
||
const formRef = ref()
|
||
|
||
const categoryOptions = ref<string[]>([])
|
||
const typeOptions = ref<string[]>([])
|
||
const companyOptions = ref<string[]>([])
|
||
|
||
const queryParams = reactive({
|
||
page: 1,
|
||
pageSize: 50,
|
||
keyword: '',
|
||
searchField: 'all',
|
||
sku: '',
|
||
category: '',
|
||
material_type: '',
|
||
company: '',
|
||
statuses: ['在库', '借库'],
|
||
orderByColumn: '',
|
||
isAsc: undefined as string | undefined,
|
||
advancedFilters: [] as any[]
|
||
})
|
||
|
||
const materialOptions = ref<any[]>([])
|
||
const searchPage = ref(1)
|
||
const searchKeyword = ref('')
|
||
const hasNextPage = ref(true)
|
||
let searchTimer: any = null
|
||
|
||
const printVisible = ref(false)
|
||
const printLoading = ref(false)
|
||
const printing = ref(false)
|
||
const previewUrl = ref('')
|
||
const currentPrintData = ref<any>({})
|
||
const printCopies = ref(1)
|
||
|
||
const entryMode = ref('batch')
|
||
const modeLocked = ref(false)
|
||
const dialogImageUrl = ref('')
|
||
const dialogVisibleImage = ref(false)
|
||
const arrivalFileList = ref<any[]>([])
|
||
const reportFileList = ref<any[]>([])
|
||
const cameraDialogVisible = ref(false)
|
||
const cameraRef = ref<InstanceType<typeof WebRtcCamera> | null>(null)
|
||
const currentCameraField = ref<'arrival_photo' | 'inspection_report'>('arrival_photo')
|
||
const inspection_report_url = ref('')
|
||
|
||
// 库位级联选择器数据
|
||
const warehouseOptions = ref<any[]>([])
|
||
|
||
const advancedFilterVisible = ref(false)
|
||
const advancedConditions = ref([{ field: '', operator: '', value: '' }])
|
||
const fieldOptions = computed(() => {
|
||
const allFields = [
|
||
{ value: 'company_name', label: '所属公司', perm: 'inbound_buy:company_name' },
|
||
{ value: 'material_name', label: '名称', perm: 'inbound_buy:material_name' },
|
||
{ value: 'material_type', label: '类型', perm: 'inbound_buy:material_type' },
|
||
{ value: 'category', label: '类别', perm: 'inbound_buy:category' },
|
||
{ value: 'spec_model', label: '规格型号', perm: 'inbound_buy:spec_model' },
|
||
{ value: 'unit', label: '单位', perm: 'inbound_buy:unit' },
|
||
{ value: 'sku', label: 'SKU', perm: 'inbound_buy:sku' },
|
||
{ value: 'barcode', label: '条码', perm: 'inbound_buy:barcode' },
|
||
{ value: 'batch_number', label: '批号', perm: 'inbound_buy:sn_bn' },
|
||
{ value: 'serial_number', label: '序列号', perm: 'inbound_buy:sn_bn' },
|
||
{ value: 'warehouse_location', label: '库位', perm: 'inbound_buy:warehouse_loc' },
|
||
{ value: 'status', label: '状态', perm: 'inbound_buy:status' },
|
||
{ value: 'inspection_status', label: '到检状态', perm: 'inbound_buy:inspection_status' },
|
||
{ value: 'qty_inbound', label: '入库量', perm: 'inbound_buy:qty_inbound' },
|
||
{ value: 'qty_stock', label: '库存数', perm: 'inbound_buy:qty_stock' },
|
||
{ value: 'qty_available', label: '可用数', perm: 'inbound_buy:qty_available' },
|
||
{ value: 'unit_price', label: '不含税单价', perm: 'inbound_buy:unit_price' },
|
||
{ value: 'total_price', label: '不含税总价', perm: 'inbound_buy:total_price' },
|
||
{ value: 'tax_rate', label: '税率', perm: 'inbound_buy:tax_rate' },
|
||
{ value: 'currency', label: '币种', perm: 'inbound_buy:currency' },
|
||
{ value: 'exchange_rate', label: '汇率', perm: 'inbound_buy:exchange_rate' },
|
||
{ value: 'supplier_name', label: '供应商', perm: 'inbound_buy:supplier_name' },
|
||
{ value: 'purchaser', label: '采购人', perm: 'inbound_buy:purchaser' },
|
||
{ value: 'purchaser_email', label: '采购邮箱', perm: 'inbound_buy:purchaser_email' },
|
||
{ value: 'source_link', label: '原始链接', perm: 'inbound_buy:source_link' },
|
||
{ value: 'detail_link', label: '详情链接', perm: 'inbound_buy:detail_link' }
|
||
]
|
||
// 根据用户权限过滤
|
||
return allFields.filter(item => userStore.hasPermission(item.perm))
|
||
})
|
||
const operatorOptions = ref([
|
||
{ value: 'eq', label: '等于' },
|
||
{ value: 'ne', label: '不等于' },
|
||
{ value: 'contains', label: '包含' },
|
||
{ value: 'ge', label: '大于等于' },
|
||
{ value: 'le', label: '小于等于' }
|
||
])
|
||
|
||
// 基础列
|
||
const baseColumns = [
|
||
{prop: 'company_name', label: '所属公司'},
|
||
{prop: 'material_name', label: '名称'},
|
||
{prop: 'material_type', label: '类型'},
|
||
{prop: 'category', label: '类别'},
|
||
{prop: 'spec_model', label: '规格型号'},
|
||
{prop: 'unit', label: '单位'},
|
||
]
|
||
|
||
// 库存与商务列
|
||
const stockColumns = [
|
||
{prop: 'id', label: 'ID', minWidth: '60'},
|
||
{prop: 'base_id', label: 'BaseID', minWidth: '80'},
|
||
{prop: 'sku', label: 'SKU', minWidth: '120'},
|
||
{prop: 'inbound_date', label: '入库日期', minWidth: '120'},
|
||
{prop: 'barcode', label: '条码', minWidth: '120'},
|
||
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160'},
|
||
{prop: 'status', label: '状态', minWidth: '100'},
|
||
{prop: 'inspection_status', label: '到检', minWidth: '100'},
|
||
{prop: 'qty_inbound', label: '入库量', minWidth: '100'},
|
||
{prop: 'qty_stock', label: '库存数', minWidth: '100'},
|
||
{prop: 'qty_available', label: '可用数', minWidth: '100'},
|
||
{prop: 'warehouse_loc', label: '库位', minWidth: '120'},
|
||
|
||
{prop: 'tax_rate', label: '税率', minWidth: '80'},
|
||
{prop: 'unit_price', label: '不含税单价', minWidth: '120'},
|
||
|
||
{prop: 'total_price', label: '不含税总价', minWidth: '120'},
|
||
{prop: 'currency', label: '币种', minWidth: '80'},
|
||
{prop: 'exchange_rate', label: '汇率', minWidth: '80'},
|
||
{prop: 'supplier_name', label: '供应商', minWidth: '150'},
|
||
{prop: 'purchaser', label: '采购人', minWidth: '100'},
|
||
{prop: 'purchaser_email', label: '邮箱', minWidth: '150'},
|
||
{prop: 'source_link', label: '采购链接', minWidth: '100'},
|
||
{prop: 'detail_link', label: '详情链接', minWidth: '100'},
|
||
{prop: 'arrival_photo', label: '到货图', minWidth: '100'},
|
||
{prop: 'inspection_report', label: '检测报告', minWidth: '100'}
|
||
]
|
||
|
||
// 列与权限Code的映射关系(数据库中的code)
|
||
const permissionMap: Record<string, string> = {
|
||
id: 'inbound_buy:id',
|
||
base_id: 'inbound_buy:base_id',
|
||
company_name: 'inbound_buy:company_name',
|
||
material_name: 'inbound_buy:material_name',
|
||
material_type: 'inbound_buy:material_type',
|
||
category: 'inbound_buy:category',
|
||
spec_model: 'inbound_buy:spec_model',
|
||
unit: 'inbound_buy:unit',
|
||
sku: 'inbound_buy:sku',
|
||
inbound_date: 'inbound_buy:inbound_date',
|
||
barcode: 'inbound_buy:barcode',
|
||
sn_bn: 'inbound_buy:sn_bn',
|
||
status: 'inbound_buy:status',
|
||
inspection_status: 'inbound_buy:inspection_status',
|
||
qty_inbound: 'inbound_buy:qty_inbound',
|
||
qty_stock: 'inbound_buy:qty_stock',
|
||
qty_available: 'inbound_buy:qty_available',
|
||
warehouse_loc: 'inbound_buy:warehouse_loc',
|
||
tax_rate: 'inbound_buy:tax_rate',
|
||
unit_price: 'inbound_buy:unit_price',
|
||
total_price: 'inbound_buy:total_price',
|
||
currency: 'inbound_buy:currency',
|
||
exchange_rate: 'inbound_buy:exchange_rate',
|
||
supplier_name: 'inbound_buy:supplier_name',
|
||
purchaser: 'inbound_buy:purchaser',
|
||
purchaser_email: 'inbound_buy:purchaser_email',
|
||
source_link: 'inbound_buy:source_link',
|
||
detail_link: 'inbound_buy:detail_link',
|
||
arrival_photo: 'inbound_buy:arrival_photo',
|
||
inspection_report: 'inbound_buy:inspection_report'
|
||
}
|
||
|
||
// 初始化列显示状态(移除权限限制,添加 localStorage 支持)
|
||
const initColumnPermissions = () => {
|
||
// 生成存储键:使用用户ID或用户名,如果没有则使用浏览器唯一标识
|
||
const userId = userStore.user?.id || userStore.username || 'anonymous'
|
||
const storageKey = `inbound_buy_columns_${userId}`
|
||
|
||
// 尝试从 localStorage 读取保存的列配置
|
||
const savedColumns = localStorage.getItem(storageKey)
|
||
if (savedColumns) {
|
||
try {
|
||
const parsed = JSON.parse(savedColumns)
|
||
// 验证保存的列是否有效(存在于 allColumns 中)
|
||
const validColumns = parsed.filter((prop: string) =>
|
||
allColumns.some(col => col.prop === prop)
|
||
)
|
||
if (validColumns.length > 0) {
|
||
visibleColumnProps.value = validColumns
|
||
return
|
||
}
|
||
} catch (e) {
|
||
console.warn('Failed to parse saved columns:', e)
|
||
}
|
||
}
|
||
|
||
// 如果没有保存的配置,使用默认列
|
||
visibleColumnProps.value = defaultColumns
|
||
}
|
||
|
||
// 检查列权限(移除权限限制,始终返回 true)
|
||
const hasColumnPermission = (prop: string) => {
|
||
return true
|
||
}
|
||
|
||
const allColumns = [...baseColumns, ...stockColumns]
|
||
|
||
const defaultColumns = [
|
||
'company_name',
|
||
'material_name', 'material_type', 'category', 'spec_model', 'unit',
|
||
'inbound_date', 'sn_bn', 'warehouse_loc', 'status', 'inspection_status',
|
||
'tax_rate', 'unit_price', 'total_price',
|
||
'supplier_name', 'purchaser', 'qty_stock', 'qty_available', 'arrival_photo', 'inspection_report'
|
||
]
|
||
|
||
const visibleColumnProps = ref(defaultColumns)
|
||
|
||
// 监听列配置变化并保存到 localStorage
|
||
watch(visibleColumnProps, (newVal) => {
|
||
const userId = userStore.user?.id || userStore.username || 'anonymous'
|
||
const storageKey = `inbound_buy_columns_${userId}`
|
||
try {
|
||
localStorage.setItem(storageKey, JSON.stringify(newVal))
|
||
} catch (e) {
|
||
console.warn('Failed to save columns to localStorage:', e)
|
||
}
|
||
}, { deep: true })
|
||
|
||
const form = reactive({
|
||
id: undefined, base_id: undefined as number | undefined,
|
||
company_name: '',
|
||
material_name: '', spec_model: '', category: '', unit: '', material_type: '',
|
||
sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', inspection_status: '未检',
|
||
in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '',
|
||
unit_price: undefined as number | undefined,
|
||
post_tax_unit_price: undefined as number | undefined,
|
||
total_price: undefined as number | undefined,
|
||
tax_rate: 0,
|
||
currency: 'CNY', exchange_rate: 1.00,
|
||
supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '',
|
||
arrival_photo: [] as string[], inspection_report: [] as string[],
|
||
print_copies: 1
|
||
})
|
||
|
||
// ------------------------------------
|
||
// 建议/Autocomplete 逻辑
|
||
// ------------------------------------
|
||
const fetchSupplierSuggestions = async (query: string, cb: any) => {
|
||
if (!form.base_id) { cb([]); return }
|
||
try {
|
||
const res: any = await getSupplierSuggestions({ base_id: form.base_id })
|
||
if (res.code === 200) {
|
||
const suppliers = res.data.map((name: string) => ({ value: name }))
|
||
const filtered = query ? suppliers.filter((item: any) => item.value.toLowerCase().includes(query.toLowerCase())) : suppliers
|
||
cb(filtered)
|
||
} else { cb([]) }
|
||
} catch (e) { cb([]) }
|
||
}
|
||
const querySearchSupplier = (qs: string, cb: any) => fetchSupplierSuggestions(qs, cb)
|
||
const handleSupplierSelect = (item: any) => { form.supplier_name = item.value }
|
||
|
||
const fetchUserSuggestions = async (query: string, cb: any) => {
|
||
try {
|
||
const res: any = await getUserSuggestions({ keyword: query })
|
||
if (res.code === 200) { cb(res.data) } else { cb([]) }
|
||
} catch (e) { cb([]) }
|
||
}
|
||
const querySearchPurchaser = (qs: string, cb: any) => fetchUserSuggestions(qs, cb)
|
||
const handlePurchaserSelect = (item: any) => { form.purchaser = item.value; if (item.email) form.purchaser_email = item.email }
|
||
|
||
const fetchLinkSuggestions = async (query: string, cb: any, type: 'original' | 'detail') => {
|
||
if (!form.base_id) { cb([]); return }
|
||
try {
|
||
const res: any = await getLinkSuggestions({ base_id: form.base_id, type })
|
||
if (res.code === 200) {
|
||
const links = res.data.map((link: string) => ({ value: link }))
|
||
const filtered = query ? links.filter((item:any) => item.value.toLowerCase().includes(query.toLowerCase())) : links
|
||
cb(filtered)
|
||
} else { cb([]) }
|
||
} catch(e) { cb([]) }
|
||
}
|
||
const querySearchLinks = (qs: string, cb: any, type: 'original' | 'detail') => fetchLinkSuggestions(qs, cb, type)
|
||
|
||
const fetchLocationSuggestions = async (query: string, cb: any) => {
|
||
if (!form.base_id) { cb([]); return }
|
||
try {
|
||
const res: any = await getLocationSuggestions({ base_id: form.base_id })
|
||
if (res.code === 200) {
|
||
const locs = res.data.map((loc: string) => ({ value: loc }))
|
||
const filtered = query ? locs.filter((item:any) => item.value.toLowerCase().includes(query.toLowerCase())) : locs
|
||
cb(filtered)
|
||
} else { cb([]) }
|
||
} catch(e) { cb([]) }
|
||
}
|
||
const querySearchLocation = (qs: string, cb: any) => fetchLocationSuggestions(qs, cb)
|
||
|
||
const currencyOptions = [{value: 'CNY', desc: '人民币'}, {value: 'USD', desc: '美元'}, {value: 'EUR', desc: '欧元'}]
|
||
const querySearchCurrency = (queryString: string, cb: any) => {
|
||
const filtered = queryString ? currencyOptions.filter(item => item.value.toLowerCase().includes(queryString.toLowerCase()) || item.desc.toLowerCase().includes(queryString.toLowerCase())) : currencyOptions
|
||
cb(filtered)
|
||
}
|
||
|
||
const handleMaterialDropdownVisible = (visible: boolean) => { if (visible && materialOptions.value.length === 0) handleSearchMaterialDebounced('') }
|
||
|
||
const handleSearchMaterialDebounced = (query: string) => {
|
||
if (searchTimer) clearTimeout(searchTimer)
|
||
searchTimer = setTimeout(() => {
|
||
handleSearchMaterial(query)
|
||
}, 300)
|
||
}
|
||
|
||
const handleSearchMaterial = async (query: string) => {
|
||
searchLoading.value = true
|
||
searchKeyword.value = query
|
||
searchPage.value = 1
|
||
materialOptions.value = []
|
||
|
||
try {
|
||
const res: any = await searchMaterialBase(query, 1)
|
||
if (res.data) {
|
||
const apiResults = (res.data || []).map((i: any) => ({...i, isHistory: false}))
|
||
materialOptions.value = apiResults
|
||
hasNextPage.value = res.has_next
|
||
}
|
||
} finally { searchLoading.value = false }
|
||
}
|
||
|
||
const loadMoreMaterials = async () => {
|
||
if (searchLoading.value || loadingMore.value || !hasNextPage.value) return
|
||
loadingMore.value = true
|
||
searchPage.value += 1
|
||
try {
|
||
const res: any = await searchMaterialBase(searchKeyword.value, searchPage.value)
|
||
if (res.data && res.data.length > 0) {
|
||
const newItems = res.data.map((i: any) => ({...i, isHistory: false}))
|
||
materialOptions.value.push(...newItems)
|
||
hasNextPage.value = res.has_next
|
||
} else {
|
||
hasNextPage.value = false
|
||
}
|
||
} catch (e) {
|
||
searchPage.value -= 1
|
||
} finally {
|
||
loadingMore.value = false
|
||
}
|
||
}
|
||
|
||
const onMaterialSelected = (val: number) => {
|
||
const item = materialOptions.value.find(i => i.id === val)
|
||
if (item) {
|
||
form.company_name = item.company_name
|
||
form.material_name = item.name
|
||
form.spec_model = item.spec
|
||
form.category = item.category
|
||
form.unit = item.unit
|
||
form.material_type = item.type
|
||
checkHistoryAndSetMode(item.id)
|
||
}
|
||
}
|
||
|
||
// ------------------------------------
|
||
// 校验规则
|
||
// ------------------------------------
|
||
const validateUnique = (rule: any, value: string, callback: any) => {
|
||
if (!value) return callback()
|
||
const isDuplicate = tableData.value.some((row: any) => {
|
||
if (dialogStatus.value === 'update' && row.id === form.id) return false
|
||
if (rule.field === 'serial_number' && row.serial_number === value) return true
|
||
if (rule.field === 'batch_number' && row.batch_number === value && row.base_id === form.base_id) return true
|
||
return false
|
||
})
|
||
if (isDuplicate) callback(new Error('当前列表页存在相同编号(后端将进行全局校验)'))
|
||
else callback()
|
||
}
|
||
const validateIdentity = (rule: any, value: any, callback: any) => {
|
||
if (entryMode.value === 'serial' && !form.serial_number && rule.field === 'serial_number') callback(new Error('SN必填'))
|
||
else if (entryMode.value === 'batch' && !form.batch_number && rule.field === 'batch_number') callback(new Error('批号必填'))
|
||
else callback()
|
||
}
|
||
const rules = {
|
||
base_id: [{required: true, message: '请选择物料', trigger: 'change'}],
|
||
in_quantity: [{required: true, message: '请输入数量', trigger: 'blur'}],
|
||
serial_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}],
|
||
batch_number: [{validator: validateIdentity, trigger: 'blur'}, {validator: validateUnique, trigger: 'blur'}]
|
||
}
|
||
|
||
const checkHistoryAndSetMode = async (baseId: number) => {
|
||
try {
|
||
const res: any = await getBuyList({page: 1, pageSize: 1000})
|
||
const historyItems = (res.data.items || []).filter((item: any) => item.base_id === baseId)
|
||
|
||
if (historyItems.length > 0) {
|
||
modeLocked.value = true
|
||
const latest = historyItems.sort((a: any, b: any) => b.id - a.id)[0]
|
||
if (latest.serial_number) {
|
||
entryMode.value = 'serial'
|
||
form.serial_number = ''
|
||
form.batch_number = ''
|
||
} else {
|
||
entryMode.value = 'batch'
|
||
form.serial_number = ''
|
||
form.batch_number = incrementBatchNumber(latest.batch_number || '000000')
|
||
}
|
||
} else {
|
||
modeLocked.value = false
|
||
entryMode.value = 'batch'
|
||
form.batch_number = '000001'
|
||
}
|
||
if (formRef.value) {
|
||
formRef.value.clearValidate('serial_number')
|
||
formRef.value.clearValidate('batch_number')
|
||
}
|
||
} catch (e) {
|
||
modeLocked.value = false
|
||
entryMode.value = 'batch'
|
||
form.batch_number = '000001'
|
||
}
|
||
}
|
||
const incrementBatchNumber = (batchStr: string) => {
|
||
if (!batchStr || !/^\d+$/.test(batchStr)) return '000001'
|
||
return (parseInt(batchStr, 10) + 1).toString().padStart(6, '0')
|
||
}
|
||
const handleEntryModeChange = (val: string) => {
|
||
if (val === 'batch') {
|
||
form.serial_number = ''
|
||
form.batch_number = '000001'
|
||
if(formRef.value) formRef.value.clearValidate('serial_number')
|
||
} else {
|
||
form.batch_number = ''
|
||
if(formRef.value) formRef.value.clearValidate('batch_number')
|
||
}
|
||
}
|
||
// 价格联动计算 (精确到小数点后2位,并支持空值)
|
||
const updatePrices = (source: string) => {
|
||
const taxMultiplier = 1 + (form.tax_rate || 0) / 100;
|
||
if (source === 'pre') {
|
||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||
} else {
|
||
form.post_tax_unit_price = undefined;
|
||
}
|
||
} else if (source === 'post') {
|
||
if (form.post_tax_unit_price !== undefined && form.post_tax_unit_price !== null) {
|
||
form.unit_price = Number((form.post_tax_unit_price / taxMultiplier).toFixed(2));
|
||
} else {
|
||
form.unit_price = undefined;
|
||
}
|
||
} else if (source === 'tax') {
|
||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||
}
|
||
}
|
||
|
||
if (form.in_quantity !== undefined && form.unit_price !== undefined && form.unit_price !== null) {
|
||
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2));
|
||
} else {
|
||
form.total_price = undefined;
|
||
}
|
||
}
|
||
|
||
watch(() => [form.in_quantity, form.unit_price], () => {
|
||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||
form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2));
|
||
// 同时更新含税单价
|
||
const taxMultiplier = 1 + (form.tax_rate || 0) / 100;
|
||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||
} else {
|
||
form.total_price = undefined;
|
||
form.post_tax_unit_price = undefined;
|
||
}
|
||
})
|
||
|
||
const fetchData = async () => {
|
||
loading.value = true
|
||
try {
|
||
const params = {
|
||
...queryParams,
|
||
statuses: queryParams.statuses.join(','),
|
||
orderByColumn: queryParams.orderByColumn,
|
||
isAsc: queryParams.isAsc,
|
||
advancedFilters: JSON.stringify(queryParams.advancedFilters)
|
||
}
|
||
const res: any = await getBuyList(params)
|
||
tableData.value = res.data.items || []
|
||
total.value = res.data.total || 0
|
||
} finally { loading.value = false }
|
||
}
|
||
|
||
// 防抖即时搜索
|
||
const debouncedSearch = debounce(() => {
|
||
queryParams.page = 1
|
||
fetchData()
|
||
}, 500)
|
||
|
||
const handleInputSearch = () => {
|
||
debouncedSearch()
|
||
}
|
||
|
||
const fetchOptions = async () => {
|
||
try {
|
||
const res: any = await getFilterOptions()
|
||
if (res.code === 200) {
|
||
categoryOptions.value = res.data.categories
|
||
typeOptions.value = res.data.types
|
||
companyOptions.value = res.data.companies
|
||
}
|
||
} catch (e) {
|
||
console.error("Fetch options failed", e)
|
||
}
|
||
}
|
||
|
||
// 加载库位树数据
|
||
const loadWarehouseTree = async () => {
|
||
try {
|
||
const res = await getWarehouseTree()
|
||
if (res.code === 200) {
|
||
warehouseOptions.value = res.data || []
|
||
}
|
||
} catch (e) {
|
||
console.error('加载库位树失败', e)
|
||
}
|
||
}
|
||
|
||
const resetQuery = () => {
|
||
queryParams.keyword = ''
|
||
queryParams.searchField = 'all'
|
||
queryParams.sku = ''
|
||
queryParams.category = ''
|
||
queryParams.material_type = ''
|
||
queryParams.company = ''
|
||
queryParams.page = 1
|
||
fetchData()
|
||
}
|
||
|
||
const handleCreate = () => {
|
||
dialogStatus.value = 'create'
|
||
resetForm()
|
||
form.in_date = dayjs().format('YYYY-MM-DD')
|
||
modeLocked.value = false
|
||
entryMode.value = 'batch'
|
||
form.batch_number = ''
|
||
visible.value = true
|
||
materialOptions.value = []
|
||
}
|
||
|
||
const handleUpdate = (row: any) => {
|
||
dialogStatus.value = 'update'
|
||
resetForm()
|
||
modeLocked.value = true
|
||
Object.assign(form, {
|
||
id: row.id, base_id: row.base_id,
|
||
company_name: row.company_name,
|
||
material_name: row.material_name, spec_model: row.spec_model, category: row.category,
|
||
unit: row.unit, material_type: row.material_type, sku: row.sku, barcode: row.barcode, in_date: row.inbound_date,
|
||
warehouse_location: row.warehouse_loc, status: row.status, inspection_status: row.inspection_status,
|
||
in_quantity: Number(row.qty_inbound), stock_quantity: Number(row.qty_stock), available_quantity: Number(row.qty_available),
|
||
unit_price: (row.unit_price !== null && row.unit_price !== undefined) ? Number(row.unit_price) : undefined,
|
||
total_price: (row.total_price !== null && row.total_price !== undefined) ? Number(row.total_price) : undefined,
|
||
tax_rate: Number(row.tax_rate),
|
||
currency: row.currency, exchange_rate: Number(row.exchange_rate),
|
||
supplier_name: row.supplier_name, purchaser: row.purchaser, purchaser_email: row.purchaser_email,
|
||
source_link: row.source_link, detail_link: row.detail_link,
|
||
arrival_photo: row.arrival_photo || [], inspection_report: row.inspection_report || []
|
||
})
|
||
// 计算含税单价
|
||
if (form.unit_price !== undefined && form.unit_price !== null) {
|
||
const taxMultiplier = 1 + (form.tax_rate || 0) / 100;
|
||
form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2));
|
||
}
|
||
|
||
arrivalFileList.value = form.arrival_photo.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||
const reports = form.inspection_report || []
|
||
const reportImgs = reports.filter(r => !isExternalLink(r))
|
||
const reportLinks = reports.filter(r => isExternalLink(r))
|
||
reportFileList.value = reportImgs.map(url => ({ name: url.split('/').pop(), url: getImageUrl(url) }))
|
||
inspection_report_url.value = reportLinks.length > 0 ? reportLinks[0] : ''
|
||
if (row.serial_number) { entryMode.value = 'serial'; form.serial_number = row.serial_number; form.batch_number = '' }
|
||
else { entryMode.value = 'batch'; form.batch_number = row.batch_number; form.serial_number = '' }
|
||
materialOptions.value = [{ id: row.base_id, name: row.material_name, spec: row.spec_model, category: row.category, company_name: row.company_name }]
|
||
visible.value = true
|
||
}
|
||
|
||
const submitForm = async () => {
|
||
if (!formRef.value) return
|
||
await formRef.value.validate(async (valid: boolean) => {
|
||
if (valid) {
|
||
submitting.value = true
|
||
const finalReportList = [...form.inspection_report]
|
||
if (inspection_report_url.value && !finalReportList.includes(inspection_report_url.value)) finalReportList.push(inspection_report_url.value)
|
||
const onlyImages = finalReportList.filter(item => !isExternalLink(item))
|
||
if (inspection_report_url.value) onlyImages.push(inspection_report_url.value)
|
||
|
||
const payload = {
|
||
...form,
|
||
inspection_report: onlyImages,
|
||
in_quantity: Number(form.in_quantity || 0),
|
||
unit_price: Number(form.unit_price || 0),
|
||
post_tax_unit_price: Number(form.post_tax_unit_price || 0)
|
||
}
|
||
try {
|
||
if (dialogStatus.value === 'create') {
|
||
const res: any = await createBuyInbound(payload)
|
||
ElMessage.success('入库成功')
|
||
if (res.data) {
|
||
ElMessage.info('发送打印指令...')
|
||
try {
|
||
// [已修改] 传递用户选择的打印份数
|
||
await executePrint({ ...res.data, copies: form.print_copies });
|
||
ElMessage.success(`打印指令已发送 (x${form.print_copies})`)
|
||
}
|
||
catch (printErr: any) { ElMessage.warning('打印失败:' + (printErr.msg || '未知错误')) }
|
||
}
|
||
} else { await updateBuyInbound(form.id!, payload); ElMessage.success('更新成功') }
|
||
|
||
await fetchData()
|
||
visible.value = false
|
||
} catch (e: any) {
|
||
ElMessage.error(e.msg || '操作失败')
|
||
} finally { submitting.value = false }
|
||
}
|
||
})
|
||
}
|
||
|
||
const getImageUrl = (url: string) => {
|
||
if (!url) return ''
|
||
if (url.startsWith('http') || url.startsWith('https') || url.startsWith('blob:')) {
|
||
return url
|
||
}
|
||
const apiBase = import.meta.env.VITE_APP_BASE_API || ''
|
||
const baseUrl = apiBase.endsWith('/') ? apiBase.slice(0, -1) : apiBase
|
||
const path = url.startsWith('/') ? url : '/' + url
|
||
return baseUrl + path
|
||
}
|
||
|
||
const isExternalLink = (str: string) => { return str && (str.startsWith('http://') || str.startsWith('https://')) && !str.includes('/api/v1/common/files') }
|
||
const getImagesOnly = (list: string[]) => { return !list ? [] : list.filter(item => !isExternalLink(item)) }
|
||
const hasExternalLink = (list: string[]) => { return !list ? false : 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: 'arrival_photo' | 'inspection_report') => {
|
||
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)
|
||
const fullUrl = getImageUrl(newUrl)
|
||
const fileObj = { name: file.name, url: fullUrl, status: 'success', uid: file.uid }
|
||
if (targetField === 'arrival_photo') {
|
||
const idx = arrivalFileList.value.findIndex(f => f.uid === file.uid)
|
||
if (idx > -1) arrivalFileList.value[idx] = fileObj
|
||
else arrivalFileList.value.push(fileObj)
|
||
} else {
|
||
const idx = reportFileList.value.findIndex(f => f.uid === file.uid)
|
||
if (idx > -1) reportFileList.value[idx] = fileObj
|
||
else reportFileList.value.push(fileObj)
|
||
}
|
||
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: 'arrival_photo' | 'inspection_report') => {
|
||
try {
|
||
const filename = uploadFile.url.split('/').pop()
|
||
const urlToRemove = form[targetField].find(u => u.endsWith(filename)) || uploadFile.url
|
||
form[targetField] = form[targetField].filter(u => u !== urlToRemove)
|
||
if (!isExternalLink(urlToRemove)) {
|
||
if (filename) await deleteFile(filename)
|
||
}
|
||
ElMessage.success('已删除')
|
||
} catch (e) { console.error(e) }
|
||
}
|
||
const handlePreviewPicture = (uploadFile: any) => { dialogImageUrl.value = uploadFile.url!; dialogVisibleImage.value = true }
|
||
const triggerCamera = (field: 'arrival_photo' | 'inspection_report') => {
|
||
currentCameraField.value = field;
|
||
cameraDialogVisible.value = true;
|
||
}
|
||
|
||
const handleCameraConfirm = async (file: File) => {
|
||
if (!beforeAvatarUpload(file)) {
|
||
return
|
||
}
|
||
const loadingInstance = ElLoading.service({
|
||
lock: true,
|
||
text: '照片上传中,请稍候...',
|
||
background: 'rgba(0, 0, 0, 0.7)',
|
||
})
|
||
try {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
const res: any = await uploadFile(formData)
|
||
if (res.code === 200) {
|
||
const newUrl = res.data.url
|
||
const field = currentCameraField.value
|
||
form[field].push(newUrl)
|
||
const fileObj = { name: file.name, url: getImageUrl(newUrl) }
|
||
if (field === 'arrival_photo') {
|
||
arrivalFileList.value.push(fileObj)
|
||
} else if (field === 'inspection_report') {
|
||
reportFileList.value.push(fileObj)
|
||
}
|
||
ElMessage.success('拍照上传成功')
|
||
cameraDialogVisible.value = false
|
||
} else {
|
||
ElMessage.error(res.msg || '上传失败')
|
||
}
|
||
} catch (e: any) {
|
||
ElMessage.error('网络错误,上传失败')
|
||
} finally {
|
||
loadingInstance.close()
|
||
}
|
||
}
|
||
|
||
const addCondition = () => {
|
||
advancedConditions.value.push({ field: '', operator: '', value: '' })
|
||
}
|
||
const removeCondition = (index: number) => {
|
||
advancedConditions.value.splice(index, 1)
|
||
}
|
||
const applyAdvancedFilter = () => {
|
||
const validConditions = advancedConditions.value.filter(c => c.field && c.operator && c.value !== '')
|
||
queryParams.advancedFilters = validConditions
|
||
advancedFilterVisible.value = false
|
||
queryParams.page = 1
|
||
fetchData()
|
||
}
|
||
const resetAdvancedFilter = () => {
|
||
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||
queryParams.advancedFilters = []
|
||
advancedFilterVisible.value = false
|
||
queryParams.page = 1
|
||
fetchData()
|
||
}
|
||
const isColumnSortable = (prop: string) => {
|
||
const sortableColumns = ['company_name', 'material_name', 'material_type', 'category', 'spec_model', 'unit', 'sku', 'barcode', 'inbound_date', 'serial_number', 'batch_number', 'status', 'inspection_status', 'qty_inbound', 'qty_stock', 'qty_available', 'warehouse_loc', 'unit_price', 'total_price', 'tax_rate', 'currency', 'exchange_rate', 'supplier_name', 'purchaser', 'purchaser_email', 'source_link', 'detail_link']
|
||
return sortableColumns.includes(prop)
|
||
}
|
||
const handleSortChange = ({ column, prop, order }: any) => {
|
||
if (prop && isColumnSortable(prop)) {
|
||
queryParams.orderByColumn = prop
|
||
queryParams.isAsc = order === 'ascending' ? 'asc' : order === 'descending' ? 'desc' : undefined
|
||
} else {
|
||
queryParams.orderByColumn = ''
|
||
queryParams.isAsc = undefined
|
||
}
|
||
queryParams.page = 1
|
||
fetchData()
|
||
}
|
||
|
||
const handleDelete = async (row: any) => { try { await deleteBuyInbound(row.id); ElMessage.success('删除成功'); fetchData() } catch (e) { ElMessage.error('删除失败') } }
|
||
|
||
// ------------------------------------
|
||
// 打印逻辑
|
||
// ------------------------------------
|
||
const handlePrint = async (row: any) => {
|
||
printVisible.value = true;
|
||
printLoading.value = true;
|
||
previewUrl.value = '';
|
||
printCopies.value = 1;
|
||
|
||
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, batch_number: row.batch_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, copies: printCopies.value });
|
||
ElMessage.success('指令已发送');
|
||
printVisible.value = false
|
||
} catch (e: any) {
|
||
ElMessage.error(e.msg || '打印失败')
|
||
} finally {
|
||
printing.value = false
|
||
}
|
||
}
|
||
|
||
const resetForm = () => {
|
||
materialOptions.value = []; arrivalFileList.value = []; reportFileList.value = []; inspection_report_url.value = ''
|
||
searchPage.value = 1; hasNextPage.value = true; searchKeyword.value = '';
|
||
Object.assign(form, {
|
||
id: undefined, base_id: undefined,
|
||
company_name: '',
|
||
material_name: '', spec_model: '', category: '', unit: '', material_type: '', sku: '', barcode: '', in_date: '', serial_number: '', batch_number: '', status: '在库', inspection_status: '未检', in_quantity: 1, stock_quantity: 1, available_quantity: 1, warehouse_location: '',
|
||
unit_price: undefined, post_tax_unit_price: undefined, total_price: undefined,
|
||
tax_rate: 0,
|
||
currency: 'CNY', exchange_rate: 1.00, supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '', arrival_photo: [], inspection_report: [],
|
||
print_copies: 1
|
||
})
|
||
}
|
||
const getStatusType = (status: string) => { const map: any = {'在库': 'success', '出库': 'info', '损耗': 'danger'}; return map[status] || 'warning' }
|
||
|
||
// 列表金额显示增加千分位处理,并保留2位小数
|
||
const formatMoney = (val: any, currency = '¥') => {
|
||
const num = Number(val);
|
||
if (isNaN(num)) return '-';
|
||
const parts = num.toFixed(2).split('.');
|
||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||
return `${currency} ${parts.join('.')}`;
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 先根据权限初始化列显示状态
|
||
initColumnPermissions()
|
||
fetchData()
|
||
fetchOptions()
|
||
loadWarehouseTree()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.buy-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||
|
||
.header-container {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
background: #fff;
|
||
padding: 16px 20px;
|
||
border-radius: 4px;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.search-form-area {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.filter-item-input {
|
||
/* 输入框样式 */
|
||
}
|
||
|
||
.filter-item-select {
|
||
/* 确保下拉框高度和输入框一致 */
|
||
}
|
||
|
||
.search-btn {
|
||
background-color: #E6F1FC;
|
||
border-color: #A3D0FD;
|
||
color: #409EFF;
|
||
}
|
||
.search-btn:hover {
|
||
background-color: #409EFF;
|
||
border-color: #409EFF;
|
||
color: #fff;
|
||
}
|
||
|
||
.reset-btn {
|
||
background-color: #fff;
|
||
border: 1px solid #dcdfe6;
|
||
}
|
||
.reset-btn:hover {
|
||
border-color: #c0c4cc;
|
||
color: #606266;
|
||
}
|
||
|
||
.right-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.add-btn {
|
||
background-color: #409EFF;
|
||
border-color: #409EFF;
|
||
padding: 8px 18px;
|
||
}
|
||
|
||
.circle-btn {
|
||
color: #606266;
|
||
border-color: #dcdfe6;
|
||
}
|
||
|
||
.modern-table { border-radius: 8px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); }
|
||
:deep(.table-header-gray th) { background-color: #f8f9fb !important; color: #606266; font-weight: 600; height: 50px; }
|
||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; background: #ecf5ff; padding: 0 4px; border-radius: 4px; margin-right: 4px; font-size: 12px; }
|
||
.tag-bn { color: #67C23A; font-weight: bold; font-family: monospace; background: #f0f9eb; padding: 0 4px; border-radius: 4px; margin-right: 4px; font-size: 12px; }
|
||
.id-cell { display: flex; align-items: center; }
|
||
.id-text { font-family: monospace; color: #606266; }
|
||
.money-text { font-family: 'Consolas', monospace; color: #303133; }
|
||
.stock-num { font-weight: bold; color: #333; font-size: 15px; }
|
||
.avail-num { font-weight: bold; color: #67C23A; font-size: 15px; }
|
||
.sum-tag { margin-left: 4px; transform: scale(0.9); }
|
||
:deep(.el-dialog__body) { padding: 0; overflow: hidden; }
|
||
|
||
/* [已修改] 增加 min-height 确保弹窗即使内容少时也保持美观高度,防止被下拉框“压垮” */
|
||
.dialog-scroll-container { padding: 15px 20px; max-height: 70vh; overflow-y: auto; overflow-x: hidden; min-height: 450px; }
|
||
|
||
.stylish-form .form-card { background: #fff; border-radius: 8px; border: 1px solid #e4e7ed; margin-bottom: 15px; }
|
||
.card-title { background: #fcfcfc; padding: 10px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; font-size: 14px; color: #303133; display: flex; align-items: center; }
|
||
.card-title .icon { margin-right: 8px; font-size: 18px; color: #409EFF; }
|
||
.card-title .sub-title { font-size: 12px; color: #909399; font-weight: normal; margin-left: 10px; }
|
||
.card-content { padding: 15px 20px; }
|
||
.basic-card { border-left: 4px solid #409EFF; }
|
||
.search-tip { color: #909399; font-size: 12px; margin-left: 10px; display: flex; align-items: center; gap: 4px; }
|
||
|
||
/* 只读输入框样式:纯文本风格 */
|
||
.is-text-view :deep(.el-input__wrapper) {
|
||
box-shadow: none !important;
|
||
background-color: transparent !important;
|
||
border-bottom: 1px dashed #dcdfe6;
|
||
border-radius: 0;
|
||
padding-left: 0;
|
||
}
|
||
.is-text-view :deep(.el-input__inner) {
|
||
color: #303133;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
cursor: text;
|
||
}
|
||
|
||
.inbound-card { border-left: 4px solid #67C23A; }
|
||
.identity-panel { background: #fffbf0; border: 1px dashed #e6a23c; border-radius: 6px; padding: 12px; margin-bottom: 15px; }
|
||
.custom-radio-group { margin-bottom: 10px; }
|
||
.locked-msg { font-size: 12px; color: #e6a23c; margin-left: 15px; }
|
||
.prefix-tag { font-weight: bold; font-size: 12px; padding: 0 5px; border-radius: 4px; }
|
||
.prefix-tag.bn { color: #67C23A; background: #f0f9eb; }
|
||
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; }
|
||
.divider-text { display: flex; align-items: center; text-align: center; margin: 20px 0 15px; color: #909399; font-size: 13px; font-weight: 500; }
|
||
.divider-text::before, .divider-text::after { content: ''; flex: 1; border-bottom: 1px solid #ebeef5; }
|
||
.divider-text::before { margin-right: 15px; }
|
||
.divider-text::after { margin-left: 15px; }
|
||
.dialog-footer { display: flex; justify-content: flex-end; gap: 15px; padding: 15px 20px; background: #fff; border-top: 1px solid #ebeef5; }
|
||
|
||
/* [重点优化] 下拉框选项样式 */
|
||
.option-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 8px 0;
|
||
width: 100%;
|
||
}
|
||
/* 名称区域:占据剩余空间,但必须有 min-width: 0 以触发 ellipsis */
|
||
.opt-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
margin-right: 10px;
|
||
}
|
||
.opt-name {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: #333;
|
||
display: block;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
/* 规格区域:固定较小宽度,靠右对齐 */
|
||
.opt-meta {
|
||
width: 100px;
|
||
text-align: right;
|
||
flex-shrink: 0;
|
||
margin-right: 10px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.opt-spec {
|
||
color: #999;
|
||
font-size: 12px;
|
||
}
|
||
/* 标签区域:不收缩 */
|
||
.opt-tags {
|
||
display: flex;
|
||
gap: 5px;
|
||
flex-shrink: 0;
|
||
}
|
||
.company-tag {
|
||
font-weight: bold;
|
||
}
|
||
|
||
.total-price-input :deep(.el-input__inner) { color: #F56C6C; font-weight: bold; }
|
||
.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; }
|
||
.clickable-text:hover { color: #66b1ff; }
|
||
.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; }
|
||
|
||
/* 自定义千分位无箭头输入框样式,用于强迫症优化显示 */
|
||
:deep(.el-input-number .el-input__inner) {
|
||
text-align: left;
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
/* 针对开启 teleport 后,挂载在 body 下的 dropdown */
|
||
.long-dropdown {
|
||
width: 580px !important; /* 固定宽度,比输入框稍宽以展示更多信息 */
|
||
}
|
||
.long-dropdown .el-select-dropdown__wrap {
|
||
max-height: 320px !important; /* 限制高度,避免遮挡整个弹窗 */
|
||
}
|
||
/* [新增] 修复清除按钮被内容遮挡的问题 (Element Plus 偶发 Bug) */
|
||
.long-dropdown .el-input__suffix {
|
||
z-index: 10;
|
||
}
|
||
</style>
|