新增盘点管理路由配置注释说明

This commit is contained in:
WUSIJIAN
2026-02-11 17:00:42 +08:00
parent b7511a7342
commit d266e5f9e5
7 changed files with 1367 additions and 2 deletions

View File

@@ -0,0 +1,115 @@
import { newService } from '/@/utils/request';
// 盘点任务查询参数
export interface InventoryCountQueryParams {
title?: string;
status?: number;
countType?: number;
warehouseId?: string;
pageNum?: number;
pageSize?: number;
}
// 盘点任务创建/更新参数
export interface InventoryCountParams {
id?: string;
title: string;
description?: string;
warehouseId?: string;
zoneId?: string;
locationId?: string;
assetSkuId?: string;
countType?: number;
scope?: number;
plannedStartTime?: string;
plannedEndTime?: string;
assigneeId?: string;
participants?: string[];
remark?: string;
}
// 获取盘点任务列表
export function listInventoryCounts(params?: InventoryCountQueryParams) {
return newService({
url: '/inventory/count/listInventoryCounts',
method: 'get',
params,
});
}
// 获取盘点任务详情
export function getInventoryCount(id: string) {
return newService({
url: '/inventory/count/getInventoryCount',
method: 'get',
params: { id },
});
}
// 创建盘点任务
export function createInventoryCount(data: InventoryCountParams) {
return newService({
url: '/inventory/count/createInventoryCount',
method: 'post',
data,
});
}
// 更新盘点任务
export function updateInventoryCount(data: InventoryCountParams) {
return newService({
url: '/inventory/count/updateInventoryCount',
method: 'put',
data,
});
}
// 删除盘点任务
export function deleteInventoryCount(id: string) {
return newService({
url: '/inventory/count/deleteInventoryCount',
method: 'delete',
params: { id },
});
}
// 完成盘点
export function completeInventoryCount(id: string) {
return newService({
url: '/inventory/count/completeInventoryCount',
method: 'post',
data: { id },
});
}
// 取消盘点
export function cancelInventoryCount(id: string[], reason?: string) {
return newService({
url: '/inventory/count/cancelInventoryCount',
method: 'post',
data: { id, reason },
});
}
// 导出盘点模板
export function exportInventoryCountTemplate() {
return newService({
url: '/inventory/count/exportInventoryCountTemplate',
method: 'get',
responseType: 'blob',
});
}
// 上传盘点Excel
export function importInventoryCount(file: File) {
const formData = new FormData();
formData.append('file', file);
return newService({
url: '/inventory/count/importInventoryCount',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
});
}

View File

@@ -65,6 +65,20 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
},
];
/**
* 盘点管理路由配置(供后端菜单配置参考)
*
* 父级菜单: 库存作业 (/assets/operation)
*
* 盘点菜单配置:
* - 路由路径: /assets/operation/count
* - 组件路径: assets/operation/count/index
* - 路由名称: assetsOperationCount
* - 菜单名称: 盘点
* - 图标: ele-Document
* - API权限: api/v1/assets/operation/count
*/
export const demoRoutes: Array<RouteRecordRaw> = [
{
path: '/demo',

View File

@@ -0,0 +1,313 @@
<template>
<el-dialog
v-model="visible"
:title="form.id ? '编辑盘点任务' : '新增盘点任务'"
width="700px"
:close-on-click-modal="false"
@close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="任务名称" prop="title">
<el-input v-model="form.title" placeholder="请输入任务名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="任务描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入任务描述" maxlength="200" show-word-limit />
</el-form-item>
<el-form-item label="盘点类型" prop="countType">
<el-select v-model="form.countType" placeholder="请选择盘点类型" style="width: 100%">
<el-option label="明盘" :value="1" />
<el-option label="盲盘" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="盘点范围" prop="warehouseIds">
<div class="scope-select-box">
<div class="scope-tags" v-if="scopeDisplayText">
<el-tag type="info" size="small">{{ getScopeLabel(form.scope) }}</el-tag>
<el-tag v-for="name in selectedWarehouseNames" :key="name" size="small" style="margin-left: 4px;">{{ name }}</el-tag>
<el-tag v-for="name in selectedZoneNames" :key="name" size="small" type="success" style="margin-left: 4px;">{{ name }}</el-tag>
<el-tag v-for="name in selectedLocationNames" :key="name" size="small" type="warning" style="margin-left: 4px;">{{ name }}</el-tag>
</div>
<el-button type="primary" link @click="openScopeDrawer">
<el-icon><ele-Setting /></el-icon>
{{ scopeDisplayText ? '修改' : '选择盘点范围' }}
</el-button>
</div>
</el-form-item>
<!-- 盘点范围选择抽屉 -->
<ScopeSelectDrawer ref="scopeDrawerRef" @confirm="onScopeConfirm" />
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="计划开始" prop="plannedStartTime">
<el-date-picker
v-model="form.plannedStartTime"
type="datetime"
placeholder="请选择计划开始时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="计划结束" prop="plannedEndTime">
<el-date-picker
v-model="form.plannedEndTime"
type="datetime"
placeholder="请选择计划结束时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="负责人" prop="assigneeId">
<el-input v-model="form.assigneeId" placeholder="请输入负责人" />
</el-form-item>
<el-form-item label="参与人员" prop="participants">
<el-input v-model="participantsInput" placeholder="请输入参与人员,多人用逗号分隔" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" maxlength="500" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
import { createInventoryCount, updateInventoryCount, getInventoryCount } from '/@/api/assets/operation/count';
import ScopeSelectDrawer from './scopeSelectDrawer.vue';
const emit = defineEmits(['refresh']);
const visible = ref(false);
const loading = ref(false);
const formRef = ref<FormInstance>();
const scopeDrawerRef = ref();
// 表单数据
const form = reactive({
id: '',
title: '',
description: '',
warehouseIds: [] as string[],
zoneIds: [] as string[],
locationIds: [] as string[],
countType: undefined as number | undefined,
scope: 1 as number,
plannedStartTime: '',
plannedEndTime: '',
assigneeId: '',
participants: [] as string[],
remark: '',
});
// 选中的名称(用于显示)
const selectedWarehouseNames = ref<string[]>([]);
const selectedZoneNames = ref<string[]>([]);
const selectedLocationNames = ref<string[]>([]);
// 参与人员输入框
const participantsInput = ref('');
// 盘点范围显示文本
const scopeDisplayText = computed(() => {
return form.warehouseIds.length > 0;
});
// 获取盘点范围标签
const getScopeLabel = (scope: number) => {
const labels: Record<number, string> = { 1: '按仓库盘点', 2: '按库区盘点', 3: '按库位盘点', 4: '按SKU盘点', 5: '按资产盘点' };
return labels[scope] || '';
};
// 表单校验规则
const rules = reactive<FormRules>({
title: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
countType: [{ required: true, message: '请选择盘点类型', trigger: 'change' }],
assigneeId: [{ required: true, message: '请输入负责人', trigger: 'blur' }],
warehouseIds: [{
required: true,
message: '请选择盘点范围',
trigger: 'change',
validator: (_rule: any, _value: any, callback: any) => {
// 按SKU盘点(4)和按资产盘点(5)不需要选择仓库
if (form.scope >= 4) {
callback();
} else if (form.warehouseIds.length === 0) {
callback(new Error('请选择盘点范围'));
} else {
callback();
}
}
}],
});
// 打开弹窗
const openDialog = async (row?: any) => {
resetForm();
if (row?.id) {
try {
const res: any = await getInventoryCount(row.id);
if (res.code === 0 && res.data) {
const data = res.data;
form.id = data.id || '';
form.title = data.title || '';
form.description = data.description || '';
form.countType = data.countType;
form.scope = data.scope || 1;
form.plannedStartTime = data.plannedStartTime || '';
form.plannedEndTime = data.plannedEndTime || '';
form.assigneeId = data.assigneeId || '';
form.remark = data.remark || '';
// 处理仓库/库区/库位ID后端返回warehouseIDs前端使用warehouseIds
form.warehouseIds = Array.isArray(data.warehouseIDs) ? data.warehouseIDs : (data.warehouseIDs ? [data.warehouseIDs] : []);
form.zoneIds = Array.isArray(data.zoneIDs) ? data.zoneIDs : (data.zoneIDs ? [data.zoneIDs] : []);
form.locationIds = Array.isArray(data.locationIDs) ? data.locationIDs : (data.locationIDs ? [data.locationIDs] : []);
// 回显参与人员
if (data.participants && Array.isArray(data.participants)) {
participantsInput.value = data.participants.join(',');
}
}
} catch (error) {
console.error('获取盘点任务详情失败:', error);
}
}
visible.value = true;
};
// 重置表单
const resetForm = () => {
form.id = '';
form.title = '';
form.description = '';
form.warehouseIds = [];
form.zoneIds = [];
form.locationIds = [];
form.countType = undefined;
form.scope = 1;
form.plannedStartTime = '';
form.plannedEndTime = '';
form.assigneeId = '';
form.participants = [];
form.remark = '';
participantsInput.value = '';
selectedWarehouseNames.value = [];
selectedZoneNames.value = [];
selectedLocationNames.value = [];
formRef.value?.resetFields();
};
// 打开盘点范围选择抽屉
const openScopeDrawer = () => {
scopeDrawerRef.value?.openDrawer({
scope: form.scope,
warehouseIds: form.warehouseIds,
zoneIds: form.zoneIds,
locationIds: form.locationIds,
});
};
// 盘点范围选择确认
const onScopeConfirm = (data: any) => {
form.scope = data.scope;
form.warehouseIds = data.warehouseIds;
form.zoneIds = data.zoneIds;
form.locationIds = data.locationIds;
selectedWarehouseNames.value = data.warehouseNames;
selectedZoneNames.value = data.zoneNames;
selectedLocationNames.value = data.locationNames;
};
// 关闭弹窗
const handleClose = () => {
visible.value = false;
};
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (!valid) return;
loading.value = true;
try {
// 构建提交数据
const submitData: any = {
title: form.title,
description: form.description || undefined,
countType: form.countType,
scope: form.scope,
plannedStartTime: form.plannedStartTime || undefined,
plannedEndTime: form.plannedEndTime || undefined,
participants: participantsInput.value ? participantsInput.value.split(',').map(s => s.trim()).filter(s => s) : undefined,
remark: form.remark || undefined,
};
// 添加选中的仓库/库区/库位ID数组
if (form.warehouseIds.length > 0) submitData.warehouseIDs = form.warehouseIds;
if (form.zoneIds.length > 0) submitData.zoneIDs = form.zoneIds;
if (form.locationIds.length > 0) submitData.locationIDs = form.locationIds;
if (form.assigneeId) submitData.assigneeId = form.assigneeId;
let res: any;
if (form.id) {
submitData.id = form.id;
res = await updateInventoryCount(submitData);
} else {
res = await createInventoryCount(submitData);
}
if (res.code === 0) {
ElMessage.success(form.id ? '修改成功' : '新增成功');
emit('refresh');
handleClose();
} else {
ElMessage.error(res.message || '操作失败');
}
} catch (error) {
console.error('保存盘点任务失败:', error);
ElMessage.error('操作失败');
} finally {
loading.value = false;
}
});
};
defineExpose({
openDialog,
});
</script>
<style scoped lang="scss">
.scope-select-box {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 8px;
.scope-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,373 @@
<template>
<el-drawer
v-model="visible"
title="选择盘点范围"
direction="rtl"
size="500px"
:close-on-click-modal="false"
>
<div class="scope-select-container">
<!-- 盘点范围类型选择 -->
<div class="scope-type-section">
<div class="section-title">盘点范围类型</div>
<el-radio-group v-model="scopeType" @change="onScopeTypeChange">
<el-radio :value="1">按仓库盘点</el-radio>
<el-radio :value="2">按库区盘点</el-radio>
<el-radio :value="3">按库位盘点</el-radio>
<el-radio :value="4">按SKU盘点</el-radio>
<el-radio :value="5">按资产盘点</el-radio>
</el-radio-group>
</div>
<!-- 仓库选择 - 仅当盘点范围<=3时显示 -->
<div v-if="scopeType <= 3" class="select-section">
<div class="section-title">
选择仓库
<span class="required">*</span>
</div>
<el-select
v-model="selectedWarehouseIds"
placeholder="请选择仓库"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
@change="onWarehouseChange"
>
<el-option v-for="item in warehouseOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<!-- 库区选择 - 仅当盘点范围为2或3时显示 -->
<div v-if="scopeType === 2 || scopeType === 3" class="select-section">
<div class="section-title">
选择库区
<span v-if="scopeType >= 2" class="required">*</span>
</div>
<el-select
v-model="selectedZoneIds"
placeholder="请先选择仓库"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:disabled="selectedWarehouseIds.length === 0"
@change="onZoneChange"
>
<el-option v-for="item in zoneOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<!-- 库位选择 - 仅当盘点范围为3时显示 -->
<div v-if="scopeType === 3" class="select-section">
<div class="section-title">
选择库位
<span v-if="scopeType >= 3" class="required">*</span>
</div>
<el-select
v-model="selectedLocationIds"
placeholder="请先选择库区"
style="width: 100%"
multiple
collapse-tags
collapse-tags-tooltip
:disabled="selectedZoneIds.length === 0"
>
<el-option v-for="item in locationOptions" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<!-- 已选择的摘要 -->
<div class="selected-summary">
<div class="section-title">已选择</div>
<div class="summary-content">
<div v-if="scopeType <= 3 && selectedWarehouseIds.length > 0" class="summary-item">
<span class="label">仓库</span>
<el-tag v-for="id in selectedWarehouseIds" :key="id" size="small" style="margin: 2px;">
{{ getWarehouseName(id) }}
</el-tag>
</div>
<div v-if="(scopeType === 2 || scopeType === 3) && selectedZoneIds.length > 0" class="summary-item">
<span class="label">库区</span>
<el-tag v-for="id in selectedZoneIds" :key="id" size="small" type="success" style="margin: 2px;">
{{ getZoneName(id) }}
</el-tag>
</div>
<div v-if="scopeType === 3 && selectedLocationIds.length > 0" class="summary-item">
<span class="label">库位</span>
<el-tag v-for="id in selectedLocationIds" :key="id" size="small" type="warning" style="margin: 2px;">
{{ getLocationName(id) }}
</el-tag>
</div>
<div v-if="scopeType <= 3 && selectedWarehouseIds.length === 0" class="empty-tip">暂未选择</div>
<div v-if="scopeType >= 4" class="empty-tip">无需选择仓库</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import { listWarehouses } from '/@/api/assets/warehouse';
import { listZones } from '/@/api/assets/zone';
import { listLocations } from '/@/api/assets/location';
const emit = defineEmits(['confirm']);
const visible = ref(false);
const scopeType = ref(1);
const selectedWarehouseIds = ref<string[]>([]);
const selectedZoneIds = ref<string[]>([]);
const selectedLocationIds = ref<string[]>([]);
const warehouseOptions = ref<any[]>([]);
const zoneOptions = ref<any[]>([]);
const locationOptions = ref<any[]>([]);
// 打开抽屉
const openDrawer = async (data?: { scope: number; warehouseIds: string[]; zoneIds: string[]; locationIds: string[] }) => {
// 重置
scopeType.value = data?.scope || 1;
selectedWarehouseIds.value = data?.warehouseIds || [];
selectedZoneIds.value = data?.zoneIds || [];
selectedLocationIds.value = data?.locationIds || [];
// 加载仓库列表
await loadWarehouses();
// 如果有已选仓库,加载库区
if (selectedWarehouseIds.value.length > 0 && scopeType.value >= 2) {
await loadZones();
}
// 如果有已选库区,加载库位
if (selectedZoneIds.value.length > 0 && scopeType.value >= 3) {
await loadLocations();
}
visible.value = true;
};
// 加载仓库列表
const loadWarehouses = async () => {
try {
const res: any = await listWarehouses({ pageNum: 1, pageSize: 100 });
warehouseOptions.value = (res.data?.list || []).map((item: any) => ({
id: item.id,
name: item.warehouseName,
}));
} catch (error) {
console.error('加载仓库列表失败:', error);
}
};
// 加载库区列表
const loadZones = async () => {
if (selectedWarehouseIds.value.length === 0) {
zoneOptions.value = [];
return;
}
try {
const allZones: any[] = [];
for (const warehouseId of selectedWarehouseIds.value) {
const res: any = await listZones({ warehouseId, pageNum: 1, pageSize: 100 });
const list = res.data?.list || res.data?.items || res.data || [];
const zones = (Array.isArray(list) ? list : []).map((item: any) => ({
id: item.id || item._id,
name: item.zoneName || item.name,
warehouseId: item.warehouseId,
}));
allZones.push(...zones);
}
zoneOptions.value = allZones;
} catch (error) {
console.error('加载库区列表失败:', error);
}
};
// 加载库位列表
const loadLocations = async () => {
if (selectedZoneIds.value.length === 0) {
locationOptions.value = [];
return;
}
try {
const allLocations: any[] = [];
for (const zoneId of selectedZoneIds.value) {
const res: any = await listLocations({ zoneId, pageNum: 1, pageSize: 100 });
const list = res.data?.list || res.data?.items || res.data || [];
const locations = (Array.isArray(list) ? list : []).map((item: any) => ({
id: item.id || item._id,
name: item.locationName || item.name,
zoneId: item.zoneId,
}));
allLocations.push(...locations);
}
locationOptions.value = allLocations;
} catch (error) {
console.error('加载库位列表失败:', error);
}
};
// 盘点范围类型变化
const onScopeTypeChange = () => {
// 清空下级选择
if (scopeType.value < 2) {
selectedZoneIds.value = [];
zoneOptions.value = [];
}
if (scopeType.value < 3) {
selectedLocationIds.value = [];
locationOptions.value = [];
}
};
// 仓库变化
const onWarehouseChange = async () => {
selectedZoneIds.value = [];
selectedLocationIds.value = [];
zoneOptions.value = [];
locationOptions.value = [];
if (scopeType.value >= 2) {
await loadZones();
}
};
// 库区变化
const onZoneChange = async () => {
selectedLocationIds.value = [];
locationOptions.value = [];
if (scopeType.value >= 3) {
await loadLocations();
}
};
// 获取仓库名称
const getWarehouseName = (id: string) => {
return warehouseOptions.value.find(item => item.id === id)?.name || id;
};
// 获取库区名称
const getZoneName = (id: string) => {
return zoneOptions.value.find(item => item.id === id)?.name || id;
};
// 获取库位名称
const getLocationName = (id: string) => {
return locationOptions.value.find(item => item.id === id)?.name || id;
};
// 关闭抽屉
const handleClose = () => {
visible.value = false;
};
// 确认选择
const handleConfirm = () => {
// 验证 - 按SKU盘点(4)和按资产盘点(5)不需要选择仓库
if (scopeType.value <= 3 && selectedWarehouseIds.value.length === 0) {
ElMessage.warning('请选择仓库');
return;
}
if ((scopeType.value === 2 || scopeType.value === 3) && selectedZoneIds.value.length === 0) {
ElMessage.warning('请选择库区');
return;
}
if (scopeType.value === 3 && selectedLocationIds.value.length === 0) {
ElMessage.warning('请选择库位');
return;
}
emit('confirm', {
scope: scopeType.value,
warehouseIds: selectedWarehouseIds.value,
zoneIds: selectedZoneIds.value,
locationIds: selectedLocationIds.value,
// 用于显示的名称
warehouseNames: selectedWarehouseIds.value.map(id => getWarehouseName(id)),
zoneNames: selectedZoneIds.value.map(id => getZoneName(id)),
locationNames: selectedLocationIds.value.map(id => getLocationName(id)),
});
handleClose();
};
defineExpose({
openDrawer,
});
</script>
<style scoped>
.scope-select-container {
padding: 0 10px;
}
.scope-type-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
}
.section-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 12px;
}
.section-title .required {
color: #f56c6c;
margin-left: 4px;
}
.select-section {
margin-bottom: 20px;
}
.selected-summary {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid #ebeef5;
}
.summary-content {
background: #f5f7fa;
border-radius: 4px;
padding: 12px;
min-height: 60px;
}
.summary-item {
margin-bottom: 8px;
}
.summary-item:last-child {
margin-bottom: 0;
}
.summary-item .label {
font-size: 13px;
color: #606266;
margin-right: 8px;
}
.empty-tip {
color: #909399;
font-size: 13px;
text-align: center;
padding: 10px 0;
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<el-drawer
v-model="visible"
title="选择仓库"
direction="rtl"
size="400px"
:close-on-click-modal="false"
>
<div class="warehouse-select-container">
<div class="search-box">
<el-input v-model="searchKeyword" placeholder="搜索仓库名称" clearable>
<template #prefix>
<el-icon><ele-Search /></el-icon>
</template>
</el-input>
</div>
<div class="select-all">
<el-checkbox v-model="selectAll" :indeterminate="isIndeterminate" @change="handleSelectAll">
全选
</el-checkbox>
<span class="selected-count">已选 {{ selectedIds.length }} </span>
</div>
<div class="warehouse-list" v-loading="loading">
<el-checkbox-group v-model="selectedIds" @change="handleSelectionChange">
<div v-for="item in filteredList" :key="item.id" class="warehouse-item">
<el-checkbox :label="item.id">
<div class="warehouse-info">
<span class="warehouse-name">{{ item.warehouseName }}</span>
<span class="warehouse-code">{{ item.warehouseCode }}</span>
</div>
</el-checkbox>
</div>
</el-checkbox-group>
<el-empty v-if="filteredList.length === 0" description="暂无数据" />
</div>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm">确定</el-button>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { listWarehouses } from '/@/api/assets/warehouse';
const emit = defineEmits(['confirm']);
const visible = ref(false);
const loading = ref(false);
const searchKeyword = ref('');
const warehouseList = ref<any[]>([]);
const selectedIds = ref<string[]>([]);
// 过滤后的列表
const filteredList = computed(() => {
if (!searchKeyword.value) return warehouseList.value;
const keyword = searchKeyword.value.toLowerCase();
return warehouseList.value.filter(item =>
item.warehouseName?.toLowerCase().includes(keyword) ||
item.warehouseCode?.toLowerCase().includes(keyword)
);
});
// 全选状态
const selectAll = computed({
get: () => selectedIds.value.length === warehouseList.value.length && warehouseList.value.length > 0,
set: () => {}
});
// 半选状态
const isIndeterminate = computed(() => {
return selectedIds.value.length > 0 && selectedIds.value.length < warehouseList.value.length;
});
// 加载仓库列表
const loadWarehouseList = async () => {
loading.value = true;
try {
const res: any = await listWarehouses({ pageNum: 1, pageSize: 1000 });
warehouseList.value = res.data?.list || [];
} catch (error) {
console.error('加载仓库列表失败:', error);
} finally {
loading.value = false;
}
};
// 打开抽屉
const openDrawer = (ids: string[] = []) => {
selectedIds.value = [...ids];
loadWarehouseList();
visible.value = true;
};
// 关闭抽屉
const handleClose = () => {
visible.value = false;
};
// 全选/取消全选
const handleSelectAll = (val: boolean) => {
if (val) {
selectedIds.value = warehouseList.value.map(item => item.id);
} else {
selectedIds.value = [];
}
};
// 选择变化
const handleSelectionChange = () => {
// 触发更新
};
// 确认选择
const handleConfirm = () => {
const selectedItems = warehouseList.value.filter(item => selectedIds.value.includes(item.id));
emit('confirm', selectedIds.value, selectedItems);
handleClose();
};
// 获取选中的仓库名称
const getSelectedNames = () => {
return warehouseList.value
.filter(item => selectedIds.value.includes(item.id))
.map(item => item.warehouseName)
.join('、');
};
defineExpose({
openDrawer,
getSelectedNames,
});
</script>
<style scoped lang="scss">
.warehouse-select-container {
display: flex;
flex-direction: column;
height: 100%;
.search-box {
margin-bottom: 16px;
}
.select-all {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #ebeef5;
margin-bottom: 8px;
.selected-count {
color: #909399;
font-size: 12px;
}
}
.warehouse-list {
flex: 1;
overflow-y: auto;
.warehouse-item {
padding: 10px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.warehouse-info {
display: flex;
flex-direction: column;
.warehouse-name {
font-size: 14px;
color: #303133;
}
.warehouse-code {
font-size: 12px;
color: #909399;
margin-top: 2px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,357 @@
<template>
<div class="inventory-count-page">
<div class="inventory-count-container">
<el-card shadow="hover">
<div class="inventory-count-search mb15">
<el-form :inline="true" :model="tableData.param">
<el-form-item label="关键词">
<el-input size="default" v-model="tableData.param.keyword" placeholder="请输入关键词" clearable style="width: 200px" />
</el-form-item>
<el-form-item label="盘点类型">
<el-select size="default" v-model="tableData.param.countType" placeholder="请选择盘点类型" clearable style="width: 150px">
<el-option label="明盘" :value="1" />
<el-option label="盲盘" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select size="default" v-model="tableData.param.status" placeholder="请选择状态" clearable style="width: 120px">
<el-option label="草稿" :value="1" />
<el-option label="已计划" :value="2" />
<el-option label="进行中" :value="3" />
<el-option label="已提交" :value="4" />
<el-option label="已完成" :value="5" />
<el-option label="已取消" :value="6" />
</el-select>
</el-form-item>
<el-form-item>
<el-button size="default" type="primary" @click="getList">
<el-icon><ele-Search /></el-icon>
查询
</el-button>
<el-button size="default" @click="onResetQuery">
<el-icon><ele-Refresh /></el-icon>
重置
</el-button>
<el-button size="default" type="success" @click="onOpenAdd">
<el-icon><ele-Plus /></el-icon>
新增
</el-button>
<el-button size="default" type="warning" @click="onExportTemplate">
<el-icon><ele-Download /></el-icon>
导出模板
</el-button>
<el-upload
ref="uploadRef"
action="#"
:auto-upload="false"
:show-file-list="false"
accept=".xlsx,.xls"
:on-change="handleImportChange"
style="display: inline-block; margin-left: 10px;"
>
<el-button size="default" type="info" :loading="importLoading">
<el-icon><ele-Upload /></el-icon>
导入
</el-button>
</el-upload>
</el-form-item>
</el-form>
</div>
<el-table :data="tableData.data" style="width: 100%" v-loading="tableData.loading" border>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="countNo" label="盘点编号" width="120" show-overflow-tooltip />
<el-table-column prop="title" label="任务名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="countTypeText" label="盘点类型" width="100" align="center">
<template #default="scope">
<el-tag :type="getCountTypeTag(scope.row.countType)">{{ scope.row.countTypeText || getCountTypeLabel(scope.row.countType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="scopeText" label="盘点范围" width="100" align="center">
<template #default="scope">
{{ scope.row.scopeText || getScopeLabel(scope.row.scope) }}
</template>
</el-table-column>
<el-table-column prop="warehouseName" label="仓库" width="120" show-overflow-tooltip />
<el-table-column prop="statusText" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="getStatusTag(scope.row.status)">{{ scope.row.statusText || getStatusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="progress" label="进度" width="80" align="center">
<template #default="scope">
{{ scope.row.progress || 0 }}%
</template>
</el-table-column>
<el-table-column prop="assigneeName" label="负责人" width="100" align="center" />
<el-table-column prop="createdAt" label="创建时间" width="170" show-overflow-tooltip />
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="scope">
<el-button link type="danger" @click="onDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="mt15" style="text-align: right">
<el-pagination
v-model:current-page="tableData.param.pageNum"
v-model:page-size="tableData.param.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="tableData.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="onSizeChange"
@current-change="onCurrentChange"
/>
</div>
</el-card>
</div>
<EditInventoryCount ref="editRef" @refresh="getList" />
</div>
</template>
<script lang="ts">
export default {
name: 'assetsOperationCount',
};
</script>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listInventoryCounts, deleteInventoryCount, completeInventoryCount, cancelInventoryCount, exportInventoryCountTemplate, importInventoryCount } from '/@/api/assets/operation/count';
import type { UploadFile } from 'element-plus';
import EditInventoryCount from './component/editInventoryCount.vue';
const editRef = ref();
const uploadRef = ref();
const importLoading = ref(false);
const tableData = reactive({
data: [] as any[],
total: 0,
loading: false,
param: {
keyword: '',
countType: undefined as number | undefined,
status: undefined as number | undefined,
pageNum: 1,
pageSize: 10,
},
});
// 获取盘点类型标签
const getCountTypeTag = (type: number) => {
const tagMap: Record<number, string> = {
0: 'primary',
1: 'success',
2: 'warning',
};
return tagMap[type] || 'info';
};
// 获取盘点类型文本
const getCountTypeLabel = (type: number) => {
const labelMap: Record<number, string> = {
1: '明盘',
2: '盲盘',
};
return labelMap[type] || '未知';
};
// 获取盘点范围文本
const getScopeLabel = (scope: number) => {
const labelMap: Record<number, string> = {
1: '按仓库盘点',
2: '按库区盘点',
3: '按库位盘点',
4: '按SKU盘点',
5: '按资产盘点',
};
return labelMap[scope] || '未知';
};
// 获取状态标签
const getStatusTag = (status: number) => {
const tagMap: Record<number, string> = {
1: 'info', // 草稿
2: 'primary', // 已计划
3: 'warning', // 进行中
4: '', // 已提交
5: 'success', // 已完成
6: 'danger', // 已取消
};
return tagMap[status] || 'info';
};
// 获取状态文本
const getStatusLabel = (status: number) => {
const labelMap: Record<number, string> = {
1: '草稿',
2: '已计划',
3: '进行中',
4: '已提交',
5: '已完成',
6: '已取消',
};
return labelMap[status] || '未知';
};
// 获取列表
const getList = async () => {
tableData.loading = true;
try {
const res: any = await listInventoryCounts(tableData.param);
tableData.data = res.data?.list || [];
tableData.total = res.data?.total || 0;
} catch (error) {
console.error('获取盘点任务列表失败:', error);
} finally {
tableData.loading = false;
}
};
// 重置查询
const onResetQuery = () => {
tableData.param.keyword = '';
tableData.param.countType = undefined;
tableData.param.status = undefined;
tableData.param.pageNum = 1;
getList();
};
// 新增
const onOpenAdd = () => {
editRef.value?.openDialog();
};
// 编辑
const onEdit = (row: any) => {
editRef.value?.openDialog(row);
};
// 完成盘点
const onComplete = (row: any) => {
ElMessageBox.confirm(`确定要完成盘点任务【${row.title}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
const res: any = await completeInventoryCount(row.id);
if (res.code === 0) {
ElMessage.success('操作成功');
getList();
} else {
ElMessage.error(res.message || '操作失败');
}
} catch (error) {
console.error('完成盘点失败:', error);
}
}).catch(() => {});
};
// 取消盘点
const onCancel = (row: any) => {
ElMessageBox.prompt(`请输入取消原因`, '取消盘点任务', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPlaceholder: '请输入取消原因',
}).then(async ({ value }) => {
try {
const res: any = await cancelInventoryCount([row.id], value);
if (res.code === 0) {
ElMessage.success('操作成功');
getList();
} else {
ElMessage.error(res.message || '操作失败');
}
} catch (error) {
console.error('取消盘点失败:', error);
}
}).catch(() => {});
};
// 删除
const onDelete = (row: any) => {
ElMessageBox.confirm(`确定要删除盘点任务【${row.title}】吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
const res: any = await deleteInventoryCount(row.id);
if (res.code === 0) {
ElMessage.success('删除成功');
getList();
} else {
ElMessage.error(res.message || '删除失败');
}
} catch (error) {
console.error('删除盘点任务失败:', error);
}
}).catch(() => {});
};
// 导出模板
const onExportTemplate = async () => {
try {
const res = await exportInventoryCountTemplate();
const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = '盘点模板.xlsx';
link.click();
window.URL.revokeObjectURL(url);
ElMessage.success('导出成功');
} catch (error) {
console.error('导出模板失败:', error);
ElMessage.error('导出模板失败');
}
};
// 导入文件变化
const handleImportChange = async (file: UploadFile) => {
if (!file.raw) return;
importLoading.value = true;
try {
const res = await importInventoryCount(file.raw);
if (res.data.code === 200) {
ElMessage.success('导入成功');
getList();
} else {
ElMessage.error(res.data.msg || '导入失败');
}
} catch (error) {
console.error('导入失败:', error);
ElMessage.error('导入失败');
} finally {
importLoading.value = false;
uploadRef.value?.clearFiles();
}
};
// 分页大小变化
const onSizeChange = (size: number) => {
tableData.param.pageSize = size;
tableData.param.pageNum = 1;
getList();
};
// 页码变化
const onCurrentChange = (page: number) => {
tableData.param.pageNum = page;
getList();
};
onMounted(() => {
getList();
});
</script>
<style scoped lang="scss">
.inventory-count-page {
padding: 15px;
}
</style>