You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1461 lines
45 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<!-- 盘点计划 Section -->
<div class="content-wrapper">
<div class="page-header">
<h1 class="page-title">盘点计划</h1>
<Button type="primary" @click="showPlanModal('new')"></Button>
</div>
<!-- Plan Search Filters -->
<div class="search-filters">
<div class="filter-item">
<span class="selector-item__label">关键词:</span>
<Input v-model="planSearch.keyword" style="width: 180px" :clearable="true" placeholder="计划名称/编号" />
</div>
<div class="filter-item">
<span class="selector-item__label">计划日期:</span>
<DatePicker v-model="planSearch.dateRange" type="daterange" split-panels style="width: 200px" :clearable="true"></DatePicker>
</div>
<div class="filter-item">
<span class="selector-item__label">状态:</span>
<Select v-model="planSearch.status" style="width: 120px" :clearable="true">
<Option value="0">未开始</Option>
<Option value="1">进行中</Option>
<Option value="2">已完成</Option>
</Select>
</div>
<div class="filter-item">
<Button type="primary" @click="searchPlans">查询</Button>
<Button style="margin-left: 10px" @click="resetPlanSearch">重置</Button>
</div>
</div>
<!-- Plan Table -->
<Table :columns="planColumns" :data="planList" :loading="planLoading">
<template slot-scope="{ row }" slot="status">
<Tag :color="getStatusColor(row.status)">{{ getStatusText(row.status) }}</Tag>
</template>
<template slot-scope="{ row }" slot="action">
<div style="display: flex; gap: 8px; justify-content: center;">
<Button type="primary" size="small" style="border-radius: 6px;" @click="showInventorySelectModal(row.id)">选择对象</Button>
<Button type="primary" size="small" style="border-radius: 6px;" ghost @click="showPlanModal('edit', row)">编辑</Button>
<Button type="error" size="small" style="border-radius: 6px;" ghost @click="deletePlan(row.id)">删除</Button>
</div>
</template>
</Table>
<!-- Plan Pagination -->
<div class="pagination-container">
<el-pagination
@size-change="handlePlanPageSizeChange"
@current-change="handlePlanPageChange"
:current-page="planSearch.pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="planSearch.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="planTotal"
/>
</div>
</div>
<!-- 盘点列表 Section -->
<div class="content-wrapper" style="margin-top: 20px">
<div class="page-header">
<h1 class="page-title">盘点列表</h1>
</div>
<!-- List Search Filters -->
<div class="search-filters">
<div class="filter-item">
<span class="selector-item__label">关键词:</span>
<Input v-model="listSearch.keyword" :clearable="true" style="width: 180px" placeholder="单号/计划/区域/执行人" />
</div>
<div class="filter-item">
<span class="selector-item__label">所属计划:</span>
<Select v-model="listSearch.planId" style="width: 180px" :clearable="true">
<Option v-for="plan in planList" :key="plan.id" :value="plan.id">{{ plan.name }}</Option>
</Select>
</div>
<div class="filter-item">
<span class="selector-item__label">状态:</span>
<Select v-model="listSearch.status" style="width: 120px" :clearable="true">
<Option value="0">未盘点</Option>
<Option value="1">已盘点</Option>
</Select>
</div>
<div class="filter-item">
<Button type="primary" @click="searchList">查询</Button>
<Button style="margin-left: 10px" @click="resetListSearch">重置</Button>
</div>
</div>
<!-- Inventory List Table -->
<Table :columns="listColumns" :data="inventoryList" :loading="listLoading">
<template slot-scope="{ row }" slot="planName">
{{ row.material_infos_plan ? row.material_infos_plan.name : '-' }}
</template>
<template slot-scope="{ row }" slot="area">
{{ row.material_info ? row.material_info.suozaicangku : '-' }}
</template>
<template slot-scope="{ row }" slot="executor">
{{ row.responsible_admin ? row.responsible_admin.name : '-' }}
</template>
<template slot-scope="{ row }" slot="startTime">
{{ row.material_infos_plan ? row.material_infos_plan.start_date : '-' }}
</template>
<template slot-scope="{ row }" slot="endTime">
{{ row.material_infos_plan ? row.material_infos_plan.end_date : '-' }}
</template>
<template slot-scope="{ row }" slot="status">
<Tag :color="getInventoryStatusColor(row.status)">{{ getInventoryStatusText(row.status) }}</Tag>
</template>
<template slot-scope="{ row }" slot="action">
<div style="display: flex; gap: 8px; justify-content: center;">
<Button type="primary" size="small" style="border-radius: 6px;" @click="viewInventoryDetail(row)">查看</Button>
</div>
</template>
</Table>
<!-- List Pagination -->
<div class="pagination-container">
<el-pagination
@size-change="handleListPageSizeChange"
@current-change="handleListPageChange"
:current-page="listSearch.pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="listSearch.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="listTotal"
/>
</div>
</div>
<!-- Plan Create/Edit Modal -->
<Modal v-model="planModal.visible" :title="planModal.title">
<Form ref="planForm" :model="planModal.form" :rules="planModal.rules" width="80">
<FormItem label="计划名称" prop="name">
<Input v-model="planModal.form.name" placeholder="请输入计划名称" :clearable="true"/>
</FormItem>
<FormItem label="计划日期" prop="dateRange">
<DatePicker
v-model="planModal.form.dateRange"
type="daterange"
split-panels
style="width: 100%"
@on-clear="handleDateClear"
:clearable="true"
:editable="false"
/>
</FormItem>
<FormItem label="备注" prop="remark">
<Input v-model="planModal.form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</FormItem>
</Form>
<template slot="footer">
<Button @click="planModal.visible = false">取消</Button>
<Button type="primary" @click="handlePlanSubmit">确定</Button>
</template>
</Modal>
<!-- Inventory Selection Modal -->
<Modal v-model="materialModal.visible" title="选择盘点物资" width="900">
<div class="material-search">
<Input v-model="materialModal.keyword" placeholder="物资名称" style="width: 180px; margin-right: 10px;" />
<Select v-model="materialModal.storehouses_id" @on-change="onStorehouseTypeChange" clearable placeholder="仓库类型" style="width: 120px; margin-right: 10px;">
<Option v-for="item in storehouseTypeOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
<Select v-model="materialModal.area" @on-change="onAreaChange" clearable placeholder="所在区域" style="width: 120px; margin-right: 10px;">
<Option v-for="item in areaOptions" :key="item.value" :value="item.value">{{ item.value }}</Option>
</Select>
<Select v-model="materialModal.warehouseName" clearable placeholder="仓库名称" style="width: 140px; margin-right: 10px;">
<Option v-for="item in warehouseNameOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
<Button type="primary" style="margin-left: 10px" @click="searchMaterials">查询</Button>
<Button style="margin-left: 10px" @click="resetMaterialSearch">重置</Button>
</div>
<el-table
ref="materialTable"
:data="materialList"
v-loading="materialModal.loading"
@select="handleSelect"
@select-all="handleSelectAll"
row-key="id"
>
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
prop="zichanmingcheng"
label="物资名称">
</el-table-column>
<el-table-column
prop="guigexinghao"
label="规格型号">
</el-table-column>
<el-table-column
prop="jiliangdanwei"
label="单位">
</el-table-column>
<el-table-column
prop="inventorys_total"
label="当前库存">
<template slot-scope="scope">
{{ scope.row.inventorys_total === null ? '0' : scope.row.inventorys_total }}
</template>
</el-table-column>
<el-table-column
label="操作"
width="100">
<template slot-scope="scope">
<div style="display: flex; gap: 8px; justify-content: center;">
<el-button
:type="isSelected(scope.row) ? 'warning' : 'success'"
size="small"
style="border-radius: 6px;"
@click="toggleMaterialSelection(scope.row, scope.$index)"
>
{{ isSelected(scope.row) ? '移出计划' : '加入计划' }}
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
@size-change="handleMaterialPageSizeChange"
@current-change="handleMaterialPageChange"
:current-page="materialModal.pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="materialModal.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="materialModal.total"
/>
</div>
<template slot="footer">
<Button @click="materialModal.visible = false">取消</Button>
<Button type="primary" @click="handleMaterialSubmit">确定</Button>
</template>
</Modal>
<!-- 盘点任务详情弹窗 -->
<Modal
v-model="detailModal.visible"
title="盘点任务详情"
width="1000"
:mask-closable="true"
>
<div class="detail-content" v-if="detailModal.data">
<div class="detail-header">
<div class="detail-item">
<span class="label">盘点单号:</span>
<span class="value">{{ detailModal.data.no }}</span>
</div>
<div class="detail-item">
<span class="label">所属计划:</span>
<span class="value">{{ detailModal.data.planName }}</span>
</div>
<div class="detail-item">
<span class="label">盘点区域:</span>
<span class="value">{{ detailModal.data.area }}</span>
</div>
<div class="detail-item">
<span class="label">执行人:</span>
<span class="value">{{ detailModal.data.executor }}</span>
</div>
<div class="detail-item">
<span class="label">开始时间:</span>
<span class="value">{{ detailModal.data.startTime }}</span>
</div>
<div class="detail-item">
<span class="label">完成时间:</span>
<span class="value">{{ detailModal.data.endTime || '-' }}</span>
</div>
<div class="detail-item">
<span class="label">状态:</span>
<span class="value">
<Tag :color="getInventoryStatusColor(detailModal.data.status)">
{{ getInventoryStatusText(detailModal.data.status) }}
</Tag>
</span>
</div>
</div>
<div class="material-info-section">
<h3>物资基本信息</h3>
<div class="material-info-content">
<div class="info-item">
<span class="label">物资名称:</span>
<span class="value">{{ detailModal.data.material_info && detailModal.data.material_info.zichanmingcheng || '-' }}</span>
</div>
<div class="info-item">
<span class="label">物资代码:</span>
<span class="value">{{ detailModal.data.material_info && detailModal.data.material_info.wuzibianma || '-' }}</span>
</div>
<div class="info-item">
<span class="label">规格型号:</span>
<span class="value">{{ detailModal.data.material_info && detailModal.data.material_info.guigexinghao || '-' }}</span>
</div>
<div class="info-item">
<span class="label">计量单位:</span>
<span class="value">{{ detailModal.data.material_info && detailModal.data.material_info.jiliangdanwei || '-' }}</span>
</div>
<div class="info-item">
<span class="label">当前库存:</span>
<span class="value">{{ detailModal.data.material_info && detailModal.data.material_info.inventorys_total || '0' }}</span>
</div>
</div>
</div>
<div class="history-section">
<h3>盘点历史记录</h3>
<Table
:columns="detailColumns"
:data="detailModal.data.history || []"
:loading="detailModal.loading"
border
>
<template slot-scope="{ row }" slot="status">
<Tag :color="getInventoryStatusColor(row.status)">{{ getInventoryStatusText(row.status) }}</Tag>
</template>
<template slot-scope="{ row }" slot="photos">
<div class="photo-list" v-if="row.files && row.files.length">
<div class="photo-item" v-for="(file, index) in row.files.slice(0, 3)" :key="index">
<img :src="file.url" @click="previewImage(file.url)" />
</div>
<div class="photo-more" v-if="row.files.length > 3">
+{{ row.files.length - 3 }}
</div>
</div>
<span v-else>-</span>
</template>
</Table>
<div class="pagination-container">
<el-pagination
@size-change="handleDetailPageSizeChange"
@current-change="handleDetailPageChange"
:current-page="detailModal.pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="detailModal.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="detailModal.total"
/>
</div>
</div>
</div>
<template slot="footer">
<Button @click="detailModal.visible = false">关闭</Button>
</template>
</Modal>
<!-- 预览弹窗 -->
<div v-if="previewUrl" class="image-preview-modal" @click="closePreview">
<img :src="previewUrl" class="image-preview-large" />
</div>
</div>
</template>
<script>
import { saveStocktakingPlan, getStocktakingPlanList, deleteStocktakingPlan, getStocktakingPlanLinkList, getStocktakingPlanLinkDetail, getStocktakingHistoryList } from '@/api/system/stocktaking'
import { getStorehouseTypeList } from '@/api/system/storehouseType'
import { getparameteritem } from '@/api/system/dictionary'
import { index as getWarehouseList } from '@/api/system/baseForm'
import qs from 'qs'
import request from '@/utils/request'
export default {
data() {
return {
planSearch: {
keyword: '',
dateRange: [],
status: '',
pageIndex: 1,
pageSize: 10
},
planList: [],
planTotal: 0,
planLoading: false,
planColumns: [
{ title: '计划编号', key: 'no' },
{ title: '计划名称', key: 'name' },
{ title: '开始日期', key: 'start_date' },
{ title: '结束日期', key: 'end_date' },
{
title: '状态',
slot: 'status',
key: 'status'
},
{
title: '操作',
slot: 'action',
width: 200
}
],
listSearch: {
keyword: '',
planId: '',
status: '',
pageIndex: 1,
pageSize: 10
},
inventoryList: [],
listTotal: 0,
listLoading: false,
listColumns: [
{ title: '盘点单号', key: 'no' },
{
title: '所属计划',
slot: 'planName',
key: 'material_infos_plan.name'
},
{
title: '盘点区域',
slot: 'area',
key: 'material_info.suozaicangku'
},
{
title: '执行人',
slot: 'executor',
key: 'responsible_admin.name'
},
{
title: '开始时间',
slot: 'startTime',
key: 'material_infos_plan.start_date'
},
{
title: '完成时间',
slot: 'endTime',
key: 'material_infos_plan.end_date'
},
{
title: '状态',
slot: 'status',
key: 'status'
},
{
title: '操作',
slot: 'action',
width: 100
}
],
planModal: {
visible: false,
title: '新建盘点计划',
form: {
id: '',
name: '',
dateRange: [],
remark: ''
},
rules: {
name: [{ required: true, message: '请输入计划名称', trigger: 'blur' }],
dateRange: [
{
required: true,
type: 'array',
min: 2,
message: '请选择计划日期',
trigger: 'change',
validator: (rule, value, callback) => {
if (!value || value.length !== 2) {
callback(new Error('请选择计划日期'));
} else {
callback();
}
}
}
]
}
},
materialModal: {
visible: false,
loading: false,
keyword: '',
storehouses_id: '',
area: '',
warehouseName: '',
currentPlanId: '',
pageIndex: 1,
pageSize: 10,
total: 0,
selectedMaterialIds: new Set(),
isInitialLoad: true
},
materialList: [],
materialColumns: [
{ type: 'selection', width: 60 },
{ title: '物资名称', key: 'zichanmingcheng' },
{ title: '规格型号', key: 'guigexinghao' },
{ title: '单位', key: 'jiliangdanwei' },
{
title: '当前库存',
key: 'inventorys_total',
render: (h, params) => {
return h('span', params.row.inventorys_total === null ? '0' : params.row.inventorys_total)
}
},
{
title: '操作',
slot: 'action',
width: 100
}
],
planMaterials: {
'P202504001': ['M002', 'M004'],
'P202505001': ['M001', 'M003', 'M006']
},
storehouseTypeOptions: [],
areaOptions: [],
warehouseNameOptions: [],
selectedRows: [],
detailModal: {
visible: false,
loading: false,
data: null,
pageIndex: 1,
pageSize: 10,
total: 0
},
detailColumns: [
{
title: '盘点日期',
key: 'check_date',
width: 180,
align: 'center'
},
{
title: '盘点数量',
key: 'check_num',
width: 100,
align: 'center'
},
{
title: '状态',
slot: 'status',
key: 'status',
width: 100,
align: 'center'
},
{
title: '备注',
key: 'remark',
minWidth: 200,
tooltip: true,
align: 'center'
},
{
title: '照片',
slot: 'photos',
width: 200,
align: 'center'
}
],
previewUrl: ''
}
},
mounted() {
this.searchPlans()
this.searchList()
this.initMaterialSelectOptions()
},
methods: {
getStatusColor(status) {
const colors = {
'0': 'red',
'1': 'orange',
'2': 'green'
}
return colors[status] || 'default'
},
getStatusText(status) {
const texts = {
'0': '未开始',
'1': '进行中',
'2': '已完成'
}
return texts[status] || status
},
getInventoryStatusColor(status) {
const colors = {
'0': 'red',
'1': 'green'
}
return colors[status] || 'default'
},
getInventoryStatusText(status) {
const texts = {
'0': '未盘点',
'1': '已盘点'
}
return texts[status] || status
},
async searchPlans() {
this.planLoading = true;
try {
const formData = new FormData();
formData.append('page', this.planSearch.pageIndex);
formData.append('page_size', this.planSearch.pageSize);
// 只添加非空的搜索条件
if (this.planSearch.keyword) {
formData.append('keyword', this.planSearch.keyword);
}
// 添加状态过滤
if (this.planSearch.status) {
const filterIndex = this.planSearch.keyword ? 1 : 0;
formData.append(`filter[${filterIndex}][key]`, 'status');
formData.append(`filter[${filterIndex}][op]`, 'eq');
formData.append(`filter[${filterIndex}][value]`, this.planSearch.status);
}
// 确保日期范围有效且不为空
if (this.planSearch.dateRange &&
this.planSearch.dateRange.length === 2 &&
this.planSearch.dateRange[0] &&
this.planSearch.dateRange[1]) {
const formatDate = (date) => {
const d = new Date(date);
if (isNaN(d.getTime())) return null; // 检查日期是否有效
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const startDate = formatDate(this.planSearch.dateRange[0]);
const endDate = formatDate(this.planSearch.dateRange[1]);
// 只有当两个日期都有效时才添加过滤条件
if (startDate && endDate) {
const filterIndex = (this.planSearch.keyword ? 1 : 0) + (this.planSearch.status ? 1 : 0);
formData.append(`filter[${filterIndex}][key]`, 'start_date');
formData.append(`filter[${filterIndex}][op]`, 'range');
formData.append(`filter[${filterIndex}][value]`, startDate+','+endDate);
}
}
const res = await getStocktakingPlanList(formData);
if (res && res.list) {
this.planList = res.list.data;
this.planTotal = res.list.total;
}
} catch (error) {
this.$Message.error('获取盘点计划列表失败');
console.error('获取盘点计划列表失败:', error);
} finally {
this.planLoading = false;
}
},
resetPlanSearch() {
this.planSearch = {
keyword: '',
dateRange: [],
status: '',
pageIndex: 1,
pageSize: 10
}
this.searchPlans()
},
handlePlanPageChange(page) {
this.planSearch.pageIndex = page
this.searchPlans()
},
handlePlanPageSizeChange(size) {
this.planSearch.pageSize = size
this.planSearch.pageIndex = 1
this.searchPlans()
},
showPlanModal(mode, plan) {
this.planModal.title = mode === 'new' ? '新建盘点计划' : '编辑盘点计划'
if (mode === 'edit' && plan) {
this.planModal.form = {
id: plan.id,
name: plan.name,
dateRange: [plan.start_date, plan.end_date],
remark: plan.remark
}
this.planModal.visible = true
} else {
this.planModal.form = {
id: '',
name: '',
dateRange: [],
remark: ''
}
this.planModal.visible = true
this.$nextTick(() => {
if (this.$refs.planForm) {
this.$refs.planForm.resetFields();
}
});
}
},
handleDateClear() {
this.planModal.form.dateRange = null;
this.$nextTick(() => {
this.planModal.form.dateRange = [];
this.$refs.planForm.validateField('dateRange');
});
},
async handlePlanSubmit() {
this.$refs.planForm.validateField('dateRange', (errorMessage) => {
if (errorMessage) {
this.$Message.error(errorMessage);
return;
}
});
this.$refs.planForm.validate(async (valid) => {
if (!valid) {
this.$Message.error('请完整填写必填项');
return;
}
const { id, name, dateRange, remark } = this.planModal.form;
const formatDate = (date) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// 获取当前计划选中的物资ID
const selectedMaterialIds = this.planMaterials[id] || [];
// 构建 formdata 格式的数据
const formData = new FormData();
formData.append('name', name);
formData.append('start_date', formatDate(dateRange[0]));
formData.append('end_date', formatDate(dateRange[1]));
if (remark) formData.append('remark', remark);
if (id) formData.append('id', id);
// 添加选中的物资ID数组
selectedMaterialIds.forEach((materialId, index) => {
formData.append(`material_infos_plan_links[${index}][material_info_id]`, materialId);
});
try {
await saveStocktakingPlan(formData);
this.$Message.success('保存成功');
this.planModal.visible = false;
this.$refs.planForm.resetFields();
this.searchPlans();
} catch (e) {
this.$Message.error('保存失败');
}
});
},
deletePlan(id) {
this.$confirm('确认要删除该计划吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await deleteStocktakingPlan({ id })
this.$message.success('删除成功')
this.searchPlans()
}).catch(() => {});
},
async searchList() {
this.listLoading = true;
try {
const formData = new FormData();
formData.append('page', this.listSearch.pageIndex);
formData.append('page_size', this.listSearch.pageSize);
// 只添加非空的搜索条件
if (this.listSearch.keyword) {
formData.append('keyword', this.listSearch.keyword);
}
// 计划筛选
if (this.listSearch.planId) {
const filterIndex = this.listSearch.keyword ? 1 : 0;
formData.append(`filter[${filterIndex}][key]`, 'material_infos_plan_id');
formData.append(`filter[${filterIndex}][op]`, 'eq');
formData.append(`filter[${filterIndex}][value]`, this.listSearch.planId);
}
// 状态筛选
if (this.listSearch.status) {
const filterIndex = (this.listSearch.keyword ? 1 : 0) + (this.listSearch.planId ? 1 : 0);
formData.append(`filter[${filterIndex}][key]`, 'status');
formData.append(`filter[${filterIndex}][op]`, 'eq');
formData.append(`filter[${filterIndex}][value]`, this.listSearch.status);
}
const res = await getStocktakingPlanLinkList(formData);
if (res && res.list) {
this.inventoryList = res.list.data;
this.listTotal = res.list.total;
}
} catch (error) {
this.$Message.error('获取盘点列表失败');
console.error('获取盘点列表失败:', error);
} finally {
this.listLoading = false;
}
},
resetListSearch() {
this.listSearch = {
keyword: '',
planId: '',
status: '',
pageIndex: 1,
pageSize: 10
}
this.searchList()
},
handleListPageChange(page) {
this.listSearch.pageIndex = page
this.searchList()
},
handleListPageSizeChange(size) {
this.listSearch.pageSize = size
this.listSearch.pageIndex = 1
this.searchList()
},
async viewInventoryDetail(row) {
this.detailModal.loading = true;
this.detailModal.visible = true;
this.detailModal.pageIndex = 1;
// 先用列表行数据初始化基本信息
this.detailModal.data = {
id: row.id,
no: row.no,
planName: row.material_infos_plan ? row.material_infos_plan.name : '-',
area: row.material_info ? row.material_info.suozaicangku : '-',
executor: row.responsible_admin ? row.responsible_admin.name : '-',
startTime: row.material_infos_plan ? row.material_infos_plan.start_date : '-',
endTime: row.material_infos_plan ? row.material_infos_plan.end_date : '-',
status: row.status,
material_info: row.material_info || {},
history: []
};
try {
// 1. 获取物资详情
const materialRes = await request({
url: '/api/admin/material-infos/show',
method: 'get',
params: { id: row.material_info.id }
});
if (materialRes) {
this.detailModal.data.material_info = materialRes;
}
// 2. 获取盘点历史记录
const params = {};
params['filter[0][key]'] = 'material_info_id';
params['filter[0][op]'] = 'eq';
params['filter[0][value]'] = row.material_info.id;
params['page'] = this.detailModal.pageIndex || 1;
params['page_size'] = this.detailModal.pageSize || 10;
const res = await getStocktakingHistoryList(qs.stringify(params));
if (res) {
// 更新物资信息和历史记录数据
this.detailModal.data = {
...this.detailModal.data,
history: res.data || []
};
this.detailModal.total = res.data ? res.data.length : 0;
}
} catch (error) {
this.$Message.error('获取盘点任务详情失败');
console.error('获取盘点任务详情失败:', error);
} finally {
this.detailModal.loading = false;
}
},
showInventorySelectModal(planId) {
this.materialModal.currentPlanId = planId
this.materialModal.visible = true
this.materialModal.pageIndex = 1
this.materialModal.selectedMaterialIds = new Set();
this.searchMaterials()
},
isSelected(item) {
return this.materialModal.selectedMaterialIds.has(item.id);
},
handleSelect(selection, item) {
console.log('handleSelect called', selection, item);
// 更新选中状态集合
const planId = this.materialModal.currentPlanId;
if (planId) {
if (selection.includes(item)) {
this.materialModal.selectedMaterialIds.add(item.id);
} else {
this.materialModal.selectedMaterialIds.delete(item.id);
}
// 更新界面
this.$nextTick(() => {
if (this.$refs.materialTable) {
this.materialList.forEach(row => {
this.$refs.materialTable.toggleRowSelection(row, this.materialModal.selectedMaterialIds.has(row.id));
});
}
});
}
},
handleSelectAll(selection) {
console.log('handleSelectAll called', selection);
const planId = this.materialModal.currentPlanId;
if (planId) {
// 更新选中状态集合
if (selection.length > 0) {
// 全选:将所有当前页的项添加到选中集合
this.materialList.forEach(item => {
this.materialModal.selectedMaterialIds.add(item.id);
});
} else {
// 取消全选:从选中集合中移除当前页的所有项
this.materialList.forEach(item => {
this.materialModal.selectedMaterialIds.delete(item.id);
});
}
// 更新界面
this.$nextTick(() => {
if (this.$refs.materialTable) {
// 更新复选框状态
this.materialList.forEach(row => {
this.$refs.materialTable.toggleRowSelection(row, this.materialModal.selectedMaterialIds.has(row.id));
});
}
});
}
},
toggleMaterialSelection(item, index) {
console.log('toggleMaterialSelection called', item, index);
// 更新选中状态
if (!this.isSelected(item)) {
console.log('Adding to plan');
this.materialModal.selectedMaterialIds.add(item.id);
} else {
console.log('Removing from plan');
this.materialModal.selectedMaterialIds.delete(item.id);
}
// 更新界面
this.$nextTick(() => {
if (this.$refs.materialTable) {
this.materialList.forEach(row => {
this.$refs.materialTable.toggleRowSelection(row, this.materialModal.selectedMaterialIds.has(row.id));
});
}
});
},
async searchMaterials() {
this.materialModal.loading = true;
this.materialModal.isInitialLoad = true; // 重置标志
try {
const data = {
page_size: this.materialModal.pageSize,
page: this.materialModal.pageIndex,
'materialstorages_cangkumingcheng': this.materialModal.warehouseName || '',
'materialstorages_suozaiquyu': this.materialModal.area || '',
'storehouses_id': this.materialModal.storehouses_id || '',
'filter[0][key]': 'zichanmingcheng',
'filter[0][op]': 'like',
'filter[0][value]': this.materialModal.keyword || '',
'filter[1][key]': 'wuzileixing',
'filter[1][op]': 'eq',
'filter[1][value]': '一物一码'
};
const res = await request({
url: '/api/admin/material-infos/index',
method: 'post',
data: qs.stringify(data),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (res && res.data) {
this.materialList = res.data;
this.materialModal.total = res.total || 0;
// 初始化选中状态
const planId = this.materialModal.currentPlanId;
if (planId) {
// 如果是第一次加载,初始化选中状态
if (this.materialModal.selectedMaterialIds.size === 0) {
this.materialList.forEach(item => {
if (item.material_infos_plan_link && item.material_infos_plan_link.length > 0) {
// 遍历物资绑定的所有盘点计划
const hasMatchingPlan = item.material_infos_plan_link.some(link =>
link.material_infos_plan_id === planId
);
if (hasMatchingPlan) {
console.log('item.id', item.id);
this.materialModal.selectedMaterialIds.add(item.id);
}
}
});
}
console.log('this.materialModal.selectedMaterialIds', this.materialModal.selectedMaterialIds)
}
}
} finally {
this.materialModal.loading = false;
}
},
async batchAddMaterials() {
const selection = this.materialList.filter(m => m.selected)
if (selection.length === 0) {
this.$Message.warning('请至少选择一项物资')
return
}
const planId = this.materialModal.currentPlanId
if (!this.planMaterials[planId]) {
this.planMaterials[planId] = []
}
selection.forEach(material => {
if (!this.planMaterials[planId].includes(material.id)) {
this.planMaterials[planId].push(material.id)
}
})
this.$Message.success(`已批量加入 ${selection.length} 项物资`)
this.searchMaterials()
},
async batchRemoveMaterials() {
const selection = this.materialList.filter(m => m.selected)
if (selection.length === 0) {
this.$Message.warning('请至少选择一项物资')
return
}
const planId = this.materialModal.currentPlanId
if (this.planMaterials[planId]) {
this.planMaterials[planId] = this.planMaterials[planId].filter(
id => !selection.some(m => m.id === id)
)
}
this.$Message.success(`已批量移出 ${selection.length} 项物资`)
this.searchMaterials()
},
async initMaterialSelectOptions() {
// 仓库类型
try {
const res = await getStorehouseTypeList({ page: 1, page_size: 999 })
this.storehouseTypeOptions = (res.data || []).map(item => ({
value: item.id,
label: item.name
}))
} catch (e) {
this.storehouseTypeOptions = []
}
// 区域
try {
const res = await getparameteritem('area')
this.areaOptions = (res.detail || []).map(item => ({
value: item.value,
label: item.label
}))
} catch (e) {
this.areaOptions = []
}
// 仓库名称
try {
const res = await getWarehouseList({
page: 1,
page_size: 999,
table_name: 'materialstorages'
})
this.warehouseNameOptions = (res.data || []).map(item => ({
value: item.id,
label: item.cangkumingcheng
}))
} catch (e) {
this.warehouseNameOptions = []
}
},
resetMaterialSearch() {
this.materialModal.keyword = ''
this.materialModal.storehouses_id = ''
this.materialModal.area = ''
this.materialModal.warehouseName = ''
this.materialModal.pageIndex = 1
this.searchMaterials()
},
async onStorehouseTypeChange(val) {
this.materialModal.storehouses_id = val
await this.updateWarehouseNameOptions()
},
async onAreaChange(val) {
this.materialModal.area = val
await this.updateWarehouseNameOptions()
},
async updateWarehouseNameOptions() {
// 联动过滤仓库名称
try {
const res = await getWarehouseList({
page: 1,
page_size: 999,
table_name: 'materialstorages',
filter: [
{ key: 'storehouses_id', op: 'eq', value: this.materialModal.storehouses_id || '' },
{ key: 'quyu_id', op: 'eq', value: this.materialModal.area || '' }
]
})
this.warehouseNameOptions = (res.data || []).map(item => ({
value: item.id,
label: item.cangkumingcheng
}))
} catch (e) {
this.warehouseNameOptions = []
}
},
handleMaterialPageChange(page) {
this.materialModal.pageIndex = page
this.searchMaterials()
},
handleMaterialPageSizeChange(size) {
this.materialModal.pageSize = size
this.materialModal.pageIndex = 1
this.searchMaterials()
},
async handleMaterialSubmit() {
const planId = this.materialModal.currentPlanId;
if (!planId) {
this.$Message.error('未找到计划ID');
return;
}
// 获取当前选中的物资ID
const selectedMaterialIds = Array.from(this.materialModal.selectedMaterialIds);
// 构建formdata
const formData = new FormData();
formData.append('id', planId);
selectedMaterialIds.forEach((materialId, index) => {
formData.append(`material_infos_plan_links[${index}][material_info_id]`, materialId);
});
try {
await saveStocktakingPlan(formData);
this.$Message.success('物资关联成功');
this.materialModal.visible = false;
this.searchPlans();
} catch (e) {
this.$Message.error('物资关联失败');
}
},
handleDetailPageChange(page) {
this.detailModal.pageIndex = page;
this.viewInventoryDetail(this.detailModal.data.id);
},
handleDetailPageSizeChange(size) {
this.detailModal.pageSize = size;
this.detailModal.pageIndex = 1;
this.viewInventoryDetail(this.detailModal.data.id);
},
previewImage(url) {
this.previewUrl = url;
},
closePreview() {
this.previewUrl = '';
}
},
watch: {
'materialModal.loading': {
handler(newVal, oldVal) {
// 当loading从true变为false时说明表格加载完成
if (oldVal === true && newVal === false) {
this.$nextTick(() => {
if (this.$refs.materialTable) {
this.materialList.forEach((item, index) => {
if (this.materialModal.selectedMaterialIds.has(item.id)) {
this.$refs.materialTable.toggleRowSelection(item, true);
console.log('item.id1111111111111111111111', item.id)
}
});
}
});
}
}
}
}
}
</script>
<style scoped lang="scss">
.code {
position: absolute;
left: 0;
top: 0;
background: rgba(0, 0, 0, 0.5);
width: 100%;
height: 100%;
z-index: 9999
}
#qrCode {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.content-wrapper {
background-color: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.page-header {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
font-size: 1.3rem;
font-weight: 600;
color: #333;
margin: 0;
}
.search-filters {
padding: 15px 0;
margin-bottom: 20px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 15px 20px;
}
.filter-item {
display: flex;
align-items: center;
gap: 8px;
}
.pagination-container {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
}
.el-pagination {
padding: 0;
font-weight: normal;
.el-pagination__sizes {
margin-right: 16px;
}
.el-pagination__jump {
margin-left: 16px;
}
.el-pagination__total {
margin-right: 16px;
}
.btn-prev,
.btn-next {
background: #fff;
border: 1px solid #dcdfe6;
&:hover {
color: #409eff;
}
}
.el-pager li {
background: #fff;
border: 1px solid #dcdfe6;
&:hover {
color: #409eff;
}
&.active {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
}
}
.material-search {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 10px;
}
.ivu-modal {
.ivu-modal-content {
width: 900px;
max-width: 90vw;
}
.ivu-modal-header {
padding: 16px 24px;
border-bottom: 1px solid #e8eaec;
.ivu-modal-header-inner {
font-size: 16px;
font-weight: 500;
color: #17233d;
}
}
.ivu-modal-body {
padding: 24px;
}
}
.ivu-table {
.ivu-table-cell {
padding: 8px 16px;
}
.ivu-table-header {
th {
background: #f8f8f9;
font-weight: 600;
color: #17233d;
}
}
.ivu-table-tbody {
tr {
&:hover {
td {
background: #f5f7fa;
}
}
}
}
}
.detail-content {
padding: 20px;
.detail-header {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
margin-bottom: 30px;
.detail-item {
.label {
font-weight: bold;
color: #666;
margin-right: 10px;
}
.value {
color: #333;
}
}
}
.material-info-section {
margin-bottom: 30px;
h3 {
margin: 0 0 15px 0;
font-size: 16px;
color: #17233d;
}
.material-info-content {
background: #f8f8f9;
padding: 20px;
border-radius: 8px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
.info-item {
.label {
font-weight: bold;
color: #666;
margin-right: 10px;
}
.value {
color: #333;
}
}
}
}
.history-section {
h3 {
margin: 0 0 15px 0;
font-size: 16px;
color: #17233d;
}
}
}
.photo-list {
display: flex;
gap: 8px;
align-items: center;
.photo-item {
width: 40px;
height: 40px;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.photo-more {
color: #666;
font-size: 12px;
}
}
.image-preview-modal {
position: fixed;
z-index: 2000;
left: 0; top: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
align-items: center;
justify-content: center;
}
.image-preview-large {
max-width: 90vw;
max-height: 90vh;
border-radius: 8px;
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
background: #fff;
}
</style>