完成盘点计划

master
lynn 7 months ago
parent f3e70818b4
commit 177152c16b

@ -40,31 +40,31 @@ export const constantRoutes = [{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/stocks',
component: () => import('@/views/stocks/index'),
hidden: true
},
{
path: '/outbounds',
component: () => import('@/views/outbounds/index'),
hidden: true
},
// {
// path: '/inventorys',
// component: () => import('@/views/inventorys/index'),
// hidden: true
// },
// {
// path: '/inventorys/stocks',
// component: () => import('@/views/inventorys/stocks'),
// hidden: true
// },
// {
// path: '/inventorys/outbounds',
// component: () => import('@/views/inventorys/outbounds'),
// hidden: true
},
{
path: '/stocks',
component: () => import('@/views/stocks/index'),
hidden: true
},
{
path: '/outbounds',
component: () => import('@/views/outbounds/index'),
hidden: true
},
// {
// path: '/inventorys',
// component: () => import('@/views/inventorys/index'),
// hidden: true
// },
// {
// path: '/inventorys/stocks',
// component: () => import('@/views/inventorys/stocks'),
// hidden: true
// },
// {
// path: '/inventorys/outbounds',
// component: () => import('@/views/inventorys/outbounds'),
// hidden: true
// },
{
path: '/info',
@ -102,8 +102,6 @@ export const constantRoutes = [{
* the routes that need to be dynamically loaded based on user roles
*/
export const asyncRoutes = [
// 404 page must be placed at the end !!!
{
path: '*',
@ -128,4 +126,4 @@ export function resetRouter() {
router.matcher = newRouter.matcher // reset router
}
export default router
export default router

@ -0,0 +1,891 @@
<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" placeholder="计划名称/编号" />
</div>
<div class="filter-item">
<span class="selector-item__label">计划日期:</span>
<DatePicker v-model="planSearch.dateRange" type="daterange" split-panels style="width: 200px"></DatePicker>
</div>
<div class="filter-item">
<span class="selector-item__label">状态:</span>
<Select v-model="planSearch.status" style="width: 120px">
<Option value="">全部</Option>
<Option value="pending">未开始</Option>
<Option value="progress">进行中</Option>
<Option value="completed">已完成</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" style="width: 180px" placeholder="单号/计划/区域/执行人" />
</div>
<div class="filter-item">
<span class="selector-item__label">所属计划:</span>
<Select v-model="listSearch.planId" style="width: 180px">
<Option value="">全部计划</Option>
<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">
<Option value="">全部</Option>
<Option value="pending">未盘点</Option>
<Option value="completed">已盘点</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="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.id)"></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" @on-ok="handlePlanSubmit">
<Form ref="planForm" :model="planModal.form" :rules="planModal.rules" label-width="80">
<FormItem label="计划名称" prop="name">
<Input v-model="planModal.form.name" placeholder="请输入计划名称" />
</FormItem>
<FormItem label="计划日期" prop="dateRange">
<DatePicker v-model="planModal.form.dateRange" type="daterange" split-panels style="width: 100%"></DatePicker>
</FormItem>
<FormItem label="备注" prop="remark">
<Input v-model="planModal.form.remark" type="textarea" :rows="3" placeholder="请输入备注信息" />
</FormItem>
</Form>
</Modal>
<!-- Inventory Selection Modal -->
<Modal v-model="materialModal.visible" title="选择盘点物资" width="800">
<div class="material-search">
<Input v-model="materialModal.keyword" placeholder="物资名称/规格" style="width: 200px" />
<Button type="primary" style="margin-left: 10px" @click="searchMaterials"></Button>
<div style="float: right">
<Button type="success" size="small" @click="batchAddMaterials"></Button>
<Button type="error" size="small" style="margin-left: 5px" @click="batchRemoveMaterials"></Button>
</div>
</div>
<Table
:columns="materialColumns"
:data="materialList"
:loading="materialModal.loading"
@on-selection-change="handleMaterialSelectionChange"
>
<template slot-scope="{ row }" slot="action">
<div style="display: flex; gap: 8px; justify-content: center;">
<Button
:type="row.selected ? 'warning' : 'success'"
size="small"
style="border-radius: 6px;"
@click="toggleMaterialSelection(row)"
>
{{ row.selected ? '移出计划' : '加入计划' }}
</Button>
</div>
</template>
</Table>
</Modal>
</div>
</template>
<script>
export default {
data() {
return {
//
planSearch: {
keyword: '',
dateRange: [],
status: '',
pageIndex: 1,
pageSize: 10
},
planList: [
{
id: 'P202504001',
name: '2025年4月库房盘点',
startDate: '2025-04-01',
endDate: '2025-04-30',
status: 'progress',
remark: '全库房范围,月底前完成'
},
{
id: 'P202505001',
name: '2025年5月A区耗材盘点',
startDate: '2025-05-01',
endDate: '2025-05-15',
status: 'pending',
remark: ''
}
],
planTotal: 2,
planLoading: false,
planColumns: [
{ title: '计划编号', key: 'id' },
{ title: '计划名称', key: 'name' },
{ title: '开始日期', key: 'startDate' },
{ title: '结束日期', key: 'endDate' },
{
title: '状态',
slot: 'status',
key: 'status'
},
{
title: '操作',
slot: 'action',
width: 200
}
],
//
listSearch: {
keyword: '',
planId: '',
status: '',
pageIndex: 1,
pageSize: 10
},
inventoryList: [
{
id: 'I202504001',
planName: '2025年4月库房盘点',
area: 'A栋一层库房',
executor: '张三',
startTime: '2025-04-10 09:00',
endTime: '-',
status: 'pending'
},
{
id: 'I202504002',
planName: '2025年4月库房盘点',
area: 'B栋地下室库房',
executor: '李四',
startTime: '2025-04-11 10:30',
endTime: '2025-04-11 15:45',
status: 'completed'
},
{
id: 'I202505001',
planName: '2025年5月A区耗材盘点',
area: 'A栋二层耗材柜',
executor: '王五',
startTime: '2025-05-05 14:00',
endTime: '-',
status: 'pending'
}
],
listTotal: 3,
listLoading: false,
listColumns: [
{ title: '盘点单号', key: 'id' },
{ title: '所属计划', key: 'planName' },
{ title: '盘点区域', key: 'area' },
{ title: '执行人', key: 'executor' },
{ title: '开始时间', key: 'startTime' },
{ title: '完成时间', key: 'endTime' },
{
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, message: '请选择计划日期', trigger: 'change' }]
}
},
//
materialModal: {
visible: false,
loading: false,
keyword: '',
currentPlanId: ''
},
materialList: [
{
id: 'M001',
name: '铁锹',
spec: '工兵锹',
unit: '把',
stock: 50,
selected: false
},
{
id: 'M002',
name: '沙袋',
spec: '编织袋 50kg',
unit: '个',
stock: 2000,
selected: true
},
{
id: 'M003',
name: '救生衣',
spec: '成人款',
unit: '件',
stock: 100,
selected: false
},
{
id: 'M004',
name: '发电机',
spec: '5kW 汽油',
unit: '台',
stock: 5,
selected: true
},
{
id: 'M005',
name: '水泵',
spec: '3寸柴油',
unit: '台',
stock: 10,
selected: false
},
{
id: 'M006',
name: '照明灯具',
spec: '强光手电',
unit: '个',
stock: 30,
selected: false
},
{
id: 'M007',
name: '雨衣',
spec: '加厚款',
unit: '套',
stock: 150,
selected: false
}
],
materialColumns: [
{ type: 'selection', width: 60 },
{ title: '物资名称', key: 'name' },
{ title: '规格型号', key: 'spec' },
{ title: '单位', key: 'unit' },
{ title: '当前库存', key: 'stock' },
{
title: '操作',
slot: 'action',
width: 100
}
],
//
planMaterials: {
'P202504001': ['M002', 'M004'], // 1
'P202505001': ['M001', 'M003', 'M006'] // 2
}
}
},
mounted() {
this.searchPlans()
this.searchList()
},
methods: {
//
getStatusColor(status) {
const colors = {
pending: 'red',
progress: 'orange',
completed: 'green'
}
return colors[status] || 'default'
},
getStatusText(status) {
const texts = {
pending: '未开始',
progress: '进行中',
completed: '已完成'
}
return texts[status] || status
},
getInventoryStatusColor(status) {
const colors = {
pending: 'red',
completed: 'green'
}
return colors[status] || 'default'
},
getInventoryStatusText(status) {
const texts = {
pending: '未盘点',
completed: '已盘点'
}
return texts[status] || status
},
//
async searchPlans() {
this.planLoading = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 500))
// 使
let filteredPlans = [...this.planList]
//
if (this.planSearch.keyword) {
const keyword = this.planSearch.keyword.toLowerCase()
filteredPlans = filteredPlans.filter(plan =>
plan.name.toLowerCase().includes(keyword) ||
plan.id.toLowerCase().includes(keyword)
)
}
//
if (this.planSearch.status) {
filteredPlans = filteredPlans.filter(plan =>
plan.status === this.planSearch.status
)
}
//
if (this.planSearch.dateRange && this.planSearch.dateRange.length === 2) {
const [start, end] = this.planSearch.dateRange
filteredPlans = filteredPlans.filter(plan =>
plan.startDate >= start && plan.endDate <= end
)
}
this.planList = filteredPlans
this.planTotal = filteredPlans.length
} 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.startDate, plan.endDate],
remark: plan.remark
}
} else {
this.planModal.form = {
id: '',
name: '',
dateRange: [],
remark: ''
}
}
this.planModal.visible = true
},
async handlePlanSubmit() {
try {
await this.$refs.planForm.validate()
// TODO: API
this.planModal.visible = false
this.searchPlans()
} catch (error) {
//
}
},
async deletePlan(id) {
try {
await this.$Modal.confirm({
title: '确认删除',
content: '确定要删除该计划吗?'
})
// TODO: API
this.searchPlans()
} catch (error) {
//
}
},
//
async searchList() {
this.listLoading = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 500))
// 使
let filteredList = [...this.inventoryList]
//
if (this.listSearch.keyword) {
const keyword = this.listSearch.keyword.toLowerCase()
filteredList = filteredList.filter(item =>
item.id.toLowerCase().includes(keyword) ||
item.planName.toLowerCase().includes(keyword) ||
item.area.toLowerCase().includes(keyword) ||
item.executor.toLowerCase().includes(keyword)
)
}
//
if (this.listSearch.planId) {
filteredList = filteredList.filter(item =>
item.planName === this.planList.find(p => p.id === this.listSearch.planId)?.name
)
}
//
if (this.listSearch.status) {
filteredList = filteredList.filter(item =>
item.status === this.listSearch.status
)
}
this.inventoryList = filteredList
this.listTotal = filteredList.length
} 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()
},
viewInventoryDetail(id) {
// TODO:
},
//
showInventorySelectModal(planId) {
this.materialModal.currentPlanId = planId
this.materialModal.visible = true
this.searchMaterials()
},
async searchMaterials() {
this.materialModal.loading = true
try {
// API
await new Promise(resolve => setTimeout(resolve, 500))
// 使
let filteredMaterials = [...this.materialList]
//
if (this.materialModal.keyword) {
const keyword = this.materialModal.keyword.toLowerCase()
filteredMaterials = filteredMaterials.filter(material =>
material.name.toLowerCase().includes(keyword) ||
material.spec.toLowerCase().includes(keyword)
)
}
//
const planMaterials = this.planMaterials[this.materialModal.currentPlanId] || []
filteredMaterials = filteredMaterials.map(material => ({
...material,
selected: planMaterials.includes(material.id)
}))
this.materialList = filteredMaterials
} finally {
this.materialModal.loading = false
}
},
handleMaterialSelectionChange(selection) {
//
},
toggleMaterialSelection(material) {
material.selected = !material.selected
const planId = this.materialModal.currentPlanId
if (!this.planMaterials[planId]) {
this.planMaterials[planId] = []
}
if (material.selected) {
if (!this.planMaterials[planId].includes(material.id)) {
this.planMaterials[planId].push(material.id)
}
} else {
this.planMaterials[planId] = this.planMaterials[planId].filter(id => id !== material.id)
}
},
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()
}
}
}
</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 {
margin-bottom: 15px;
display: flex;
align-items: center;
}
/* 按钮样式优化 */
.ivu-btn {
&.ivu-btn-small {
padding: 4px 12px;
font-size: 13px;
height: 28px;
line-height: 1.5;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
&.ivu-btn-primary {
background: #2d8cf0;
border-color: #2d8cf0;
color: #fff;
&:hover {
background: #57a3f3;
border-color: #57a3f3;
}
&.ivu-btn-ghost {
background: transparent;
color: #2d8cf0;
border-color: #2d8cf0;
&:hover {
background: rgba(45,140,240,0.1);
}
}
}
&.ivu-btn-error {
background: #ed4014;
border-color: #ed4014;
color: #fff;
&:hover {
background: #f16643;
border-color: #f16643;
}
&.ivu-btn-ghost {
background: transparent;
color: #ed4014;
border-color: #ed4014;
&:hover {
background: rgba(237,64,20,0.1);
}
}
}
&.ivu-btn-success {
background: #19be6b;
border-color: #19be6b;
color: #fff;
&:hover {
background: #47cb89;
border-color: #47cb89;
}
}
&.ivu-btn-warning {
background: #ff9900;
border-color: #ff9900;
color: #fff;
&:hover {
background: #ffad33;
border-color: #ffad33;
}
}
}
}
/* 表格操作列样式 */
.table-col-action {
.ivu-btn {
min-width: 80px;
}
}
/* 表格样式优化 */
.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;
}
}
}
}
}
</style>

@ -744,7 +744,7 @@ export default {
display: flex;
flex-direction: column;
min-height: 0;
height: calc(100vh - 200px);
height: calc(100vh - 280px);
}
.el-table {
@ -752,6 +752,7 @@ export default {
overflow: hidden;
width: 100%;
height: 100%;
margin-bottom: 10px;
}
.el-table__body-wrapper {
@ -765,7 +766,7 @@ export default {
.el-table td {
font-size: 14px;
padding: 8px 0;
padding: 12px 0;
}
.el-table th {
@ -773,7 +774,7 @@ export default {
color: #333;
font-weight: bold;
font-size: 15px;
padding: 8px 0;
padding: 12px 0;
}
.status-badge {
@ -992,7 +993,50 @@ export default {
.pagination-container {
display: flex;
justify-content: flex-end;
margin-top: 20px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
height: 50px;
}
.el-pagination {
padding: 0;
font-weight: normal;
height: 32px;
line-height: 32px;
.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;
height: 32px;
line-height: 32px;
&:hover {
color: #409eff;
}
}
.el-pager li {
background: #fff;
border: 1px solid #dcdfe6;
height: 32px;
line-height: 32px;
&:hover {
color: #409eff;
}
&.active {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
}
}
@media (max-width: 900px) {

Loading…
Cancel
Save