lion 1 year ago
commit 4972d5534e

@ -0,0 +1,67 @@
import request from '@/utils/request';
// 设备运维规则配置
export function getMaintainConfigList(params) {
return request({
url: '/api/admin/equipment-maintain-config/index',
method: 'get',
params
});
}
export function getMaintainConfigDetail(params) {
return request({
url: '/api/admin/equipment-maintain-config/show',
method: 'get',
params
});
}
export function saveMaintainConfig(data) {
return request({
url: '/api/admin/equipment-maintain-config/save',
method: 'post',
data
});
}
export function deleteMaintainConfig(params) {
return request({
url: '/api/admin/equipment-maintain-config/destroy',
method: 'get',
params
});
}
// 设备运维记录
export function getOperationList(params) {
return request({
url: '/api/admin/equipment-operation/index',
method: 'get',
params
});
}
export function getOperationDetail(params) {
return request({
url: '/api/admin/equipment-operation/show',
method: 'get',
params
});
}
export function saveOperation(data) {
return request({
url: '/api/admin/equipment-operation/save',
method: 'post',
data
});
}
export function deleteOperation(params) {
return request({
url: '/api/admin/equipment-operation/destroy',
method: 'get',
params
});
}

@ -0,0 +1,10 @@
import request from '@/utils/request';
// 绑定/更新物资与运维规则的关系
export function saveStocksItem(data) {
return request({
url: '/api/admin/stocks-item/save',
method: 'post',
data
});
}

@ -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>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,783 @@
<template>
<div class="table-page-container">
<div ref="lxHeader">
<lx-header icon="md-apps" text="维护规则" style="margin-bottom: 10px; border: 0px; margin-top: 15px">
<slot>
<div style="display: flex;justify-content: flex-end;flex-wrap: wrap;">
<Button type="primary" @click="showAddRuleModal"></Button>
</div>
</slot>
</lx-header>
</div>
<div class="table-flex-content">
<xy-table :list="list" :total="total" @pageSizeChange="pageSizeChange" @pageIndexChange="pageChange"
:table-item="table" size="medium" :height="tableHeight" style="border-radius: 10px; overflow: hidden; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.08); width: 100%;">
<template v-slot:btns>
<el-table-column label="操作" align="center" min-width="200" header-align="center" class-name="table-col-action">
<template slot-scope="scope">
<div style="display: flex; gap: 8px; justify-content: center;">
<Button type="primary" size="small" style="border-radius: 6px;" @click="handleEdit(scope.row)"></Button>
<Button type="primary" size="small" style="border-radius: 6px;" ghost @click="handleDelete(scope.row)"></Button>
<Button type="primary" size="small" style="border-radius: 6px;" ghost @click="handleAssociate(scope.row)"></Button>
</div>
</template>
</el-table-column>
</template>
</xy-table>
</div>
<!-- 新增规则 Modal -->
<Modal v-model="showModal" title="新增维护规则" :footer="null" @on-cancel="handleCancel">
<Form ref="form" :model="form" :rules="rules" :label-width="120">
<FormItem label="规则名称" prop="name">
<Input v-model="form.name" placeholder="请输入规则名称"></Input>
</FormItem>
<FormItem label="重复周期" prop="cycle">
<Select v-model="form.cycle">
<Option v-for="item in cycleOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
</FormItem>
<FormItem label="起始日期设定" prop="startSetting">
<Select v-model="form.startSetting">
<Option v-for="item in startStandardOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
</FormItem>
<FormItem label="生成计划时机" prop="timing">
<div class="timing-controls">
<Select v-model="form.timingUnit" style="width: 80px">
<Option value="days">提前</Option>
</Select>
<InputNumber v-model="form.timingValue" :min="1" style="width: 80px"></InputNumber>
<Select v-model="form.timingPeriod" style="width: 80px">
<Option value="days"></Option>
</Select>
</div>
</FormItem>
<FormItem label="未完成时下次计划" prop="nextPlan">
<Select v-model="form.nextPlan">
<Option v-for="item in nextPlanOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
</FormItem>
<FormItem label="备注" prop="notes">
<Input v-model="form.notes" type="textarea" :rows="3" placeholder="请输入备注"></Input>
</FormItem>
</Form>
<div slot="footer" style="text-align: right;">
<Button @click="handleCancel"></Button>
<Button type="primary" @click="handleCustomSubmit" style="margin-left: 8px;">确定</Button>
</div>
</Modal>
<!-- 编辑规则 Modal -->
<Modal v-model="showEditModal" title="编辑维护规则" @on-ok="handleEditSubmit" @on-cancel="handleEditCancel">
<Form ref="editForm" :model="editForm" :rules="rules" :label-width="120">
<FormItem label="规则名称" prop="name">
<Input v-model="editForm.name" placeholder="请输入规则名称"></Input>
</FormItem>
<FormItem label="重复周期" prop="cycle">
<Select v-model="editForm.cycle">
<Option v-for="item in cycleOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
</FormItem>
<FormItem label="起始日期设定" prop="startSetting">
<Select v-model="editForm.startSetting">
<Option v-for="item in startStandardOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
</FormItem>
<FormItem label="生成计划时机" prop="timing">
<div class="timing-controls">
<Select v-model="editForm.timingUnit" style="width: 80px">
<Option value="days">提前</Option>
</Select>
<InputNumber v-model="editForm.timingValue" :min="1" style="width: 80px"></InputNumber>
<Select v-model="editForm.timingPeriod" style="width: 80px">
<Option value="days"></Option>
</Select>
</div>
</FormItem>
<FormItem label="未完成时下次计划" prop="nextPlan">
<Select v-model="editForm.nextPlan">
<Option v-for="item in nextPlanOptions" :key="item.value" :value="item.value">{{ item.label }}</Option>
</Select>
</FormItem>
<FormItem label="备注" prop="notes">
<Input v-model="editForm.notes" type="textarea" :rows="3" placeholder="请输入备注"></Input>
</FormItem>
</Form>
</Modal>
<!-- 关联物资 Modal -->
<Modal v-model="showAssociateModal" title="关联物资" @on-ok="handleAssociateSubmit" @on-cancel="handleAssociateCancel" width="1000">
<div class="associate-content">
<div class="associate-toolbar">
<Input v-model="associateSearch" placeholder="搜索物资名称..." style="width: 250px; margin-right: 10px;" clearable></Input>
<Select v-model="select.storehouses_id" @on-change="getWarehouseNames" style="width: 120px; margin-right: 10px;" clearable placeholder="仓库类型">
<Option v-for="item in warehouseTypes" :key="item.id" :value="item.id">{{ item.name }}</Option>
</Select>
<Select v-model="select.area" @on-change="getWarehouseNames" style="width: 120px; margin-right: 10px;" clearable placeholder="所在区域">
<Option v-for="item in warehouseAreas" :key="item.id" :value="item.value">{{ item.value }}</Option>
</Select>
<Select v-model="associateWarehouseName" style="width: 120px; margin-right: 10px;" clearable placeholder="仓库名称">
<Option v-for="warehouse in warehouseNames" :key="warehouse.value" :value="warehouse.label">{{ warehouse.label }}</Option>
</Select>
<Button type="primary" @click="searchMaterialsPost"></Button>
<Button style="margin-left: 8px;" @click="resetAssociateSearch"></Button>
</div>
<Table :columns="associateColumns" :data="associateList" :height="400" style="margin-top: 15px;">
<template slot-scope="{ row }" slot="action">
<div style="display: flex; gap: 8px; justify-content: center;">
<Button
v-if="row.equipment_maintain_config && row.equipment_maintain_config.id === currentRule.id"
type="error"
size="small"
style="border-radius: 6px;"
@click="toggleAssociation(row)"
:title="row.equipment_maintain_config ? `已关联规则: ${row.equipment_maintain_config.name}` : ''"
>
解除绑定
</Button>
<Button
v-else-if="row.equipment_maintain_config"
type="warning"
size="small"
style="border-radius: 6px;"
@click="toggleAssociation(row)"
:title="row.equipment_maintain_config ? `已关联规则: ${row.equipment_maintain_config.name}` : ''"
>
重新绑定
</Button>
<Button
v-else
type="primary"
size="small"
style="border-radius: 6px;"
@click="toggleAssociation(row)"
>
绑定
</Button>
</div>
</template>
</Table>
<div class="pagination-container">
<el-pagination
@size-change="handleAssociatePageSizeChange"
@current-change="handleAssociatePageChange"
:current-page="associateCurrentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="associatePageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="associateTotal"
/>
</div>
</div>
</Modal>
</div>
</template>
<script>
import { Button, Modal, Form, FormItem, Input, Select, Option, InputNumber, Table, Checkbox, Page } from 'view-design';
import { getStorehouseTypeList } from '@/api/system/storehouseType';
import { getparameteritem } from "@/api/system/dictionary.js";
import { index } from "@/api/system/baseForm.js";
import { saveMaintainConfig, getMaintainConfigList, deleteMaintainConfig } from '@/api/maintenance/maintenance.js';
import { saveStocksItem } from '@/api/stocks-item.js';
import request from '@/utils/request';
import qs from 'qs';
export default {
components: {
Button,
Modal,
Form,
FormItem,
Input,
Select,
Option,
InputNumber,
Table,
Checkbox,
Page
},
data() {
return {
select: {
page: 1,
page_size: 999,
keyword: '',
table_name: 'materialstorages',
area: '',
storehouses_id: ''
},
total: 0,
list: [],
cycleOptions: [
{ value: '1', label: '每月' },
{ value: '3', label: '每季度' },
{ value: '6', label: '每半年' },
{ value: '12', label: '每年' }
],
startStandardOptions: [
{ value: '1', label: '从每年第一天开始运算' },
{ value: '2', label: '首次关联物资时' }
],
nextPlanOptions: [
{ value: '1', label: '暂停生成(直到上次计划已经执行完成)' },
{ value: '2', label: '强制生成(忽略未完成状态)' }
],
table: [
{ label: '序号', type: 'index', width: 60 },
{ label: '规则名称', prop: 'name', minWidth: 160 },
{ label: '重复周期', prop: 'month_frequency', minWidth: 120, formatter: row => this.cycleOptions.find(opt => opt.value == row.month_frequency)?.label || row.month_frequency },
{ label: '起始日期设定', prop: 'start_standard', minWidth: 160, formatter: row => this.startStandardOptions.find(opt => opt.value == row.start_standard)?.label || row.start_standard },
{ label: '生成计划时机(天)', prop: 'advance_days', minWidth: 120 },
{ label: '已用于物资数', prop: 'stocks_items_count', minWidth: 120 },
{ label: '未完成时下次计划', prop: 'next_plan_type', minWidth: 160, formatter: row => this.nextPlanOptions.find(opt => opt.value == row.next_plan_type)?.label || row.next_plan_type },
],
tableHeight: 550,
showModal: false,
showEditModal: false,
showAssociateModal: false,
form: {
name: '',
cycle: '',
startSetting: '',
timingUnit: 'days',
timingValue: 7,
timingPeriod: 'days',
nextPlan: '',
notes: ''
},
editForm: {
name: '',
cycle: '',
startSetting: '',
timingUnit: 'days',
timingValue: 7,
timingPeriod: 'days',
nextPlan: '',
notes: ''
},
rules: {
name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
cycle: [{ required: true, message: '请选择重复周期', trigger: 'change' }],
startSetting: [{ required: true, message: '请选择起始日期设定', trigger: 'change' }],
nextPlan: [{ required: true, message: '请选择未完成时下次计划', trigger: 'change' }]
},
associateSearch: '',
associateWarehouseName: '',
associatePageSize: 100,
associateCurrentPage: 1,
associateTotal: 0,
associateColumns: [
{
title: '物资名称',
key: 'zichanmingcheng',
minWidth: 120
},
{
title: '物资代码',
key: 'wuzibianma',
minWidth: 120
},
{
title: '规格型号',
key: 'guigexinghao',
minWidth: 120
},
{
title: '仓库名称',
key: 'material_info',
minWidth: 120,
render: (h, params) => {
return h('span', params.row.material_info?.suozaicangku || '');
}
},
{
title: '操作',
slot: 'action',
width: 200,
fixed: 'right',
align: 'center',
headerAlign: 'center',
className: 'table-col-action'
}
],
associateList: [],
currentRule: null,
ruleAssociations: new Map(),
warehouseTypes: [],
warehouseAreas: [],
warehouseNames: [],
allWarehouses: [],
currentWarehouseNames: []
}
},
created() {
this.getList();
this.getWarehouseTypes();
this.getWarehouseAreas();
this.getWarehouseNames();
},
mounted() {
this.calcTableHeight();
window.addEventListener('resize', this.calcTableHeight);
},
beforeDestroy() {
window.removeEventListener('resize', this.calcTableHeight);
},
watch: {
associateWarehouseArea(val) {
if (val) {
this.getWarehouseNames(val);
} else {
this.warehouseNames = [];
}
}
},
methods: {
async getList() {
try {
const res = await getMaintainConfigList({
page: this.select.page,
page_size: this.select.page_size,
//
});
if (res && res.data) {
this.list = res.data;
this.total = res.total;
}
} catch (e) {
this.$message.error('获取运维规则列表失败');
}
},
pageChange(e) {
this.select.page = e
this.getList()
},
pageSizeChange(e) {
this.select.page_size = e
this.getList()
},
calcTableHeight() {
const header = this.$refs.lxHeader ? this.$refs.lxHeader.offsetHeight : 0;
const windowHeight = window.innerHeight;
const minTableHeight = 600;
this.tableHeight = Math.max(windowHeight - header - 100, minTableHeight);
},
showAddRuleModal() {
this.form = {
name: '',
cycle: '',
startSetting: '',
timingUnit: 'days',
timingValue: 7,
timingPeriod: 'days',
nextPlan: '',
notes: ''
};
this.showModal = true;
this.$nextTick(() => {
if (this.$refs.form) {
this.$refs.form.resetFields();
}
});
},
handleCancel() {
this.showModal = false;
this.$refs.form.resetFields();
},
handleEdit(row) {
this.currentRule = row;
this.editForm = {
name: row.name,
cycle: String(row.month_frequency),
startSetting: String(row.start_standard),
timingUnit: 'days',
timingValue: row.advance_days,
timingPeriod: 'days',
nextPlan: String(row.next_plan_type),
notes: row.remark || ''
};
this.showEditModal = true;
},
handleEditSubmit() {
this.$refs.editForm.validate((valid) => {
if (valid) {
// TODO:
this.$message.success('编辑成功');
this.showEditModal = false;
this.getList();
}
});
},
handleEditCancel() {
this.showEditModal = false;
this.$refs.editForm.resetFields();
},
handleDelete(row) {
this.currentRule = row;
this.$confirm('确认要删除该维护规则吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
await deleteMaintainConfig({ id: this.currentRule.id });
this.$message.success('删除成功');
this.getList();
} catch (e) {
this.$message.error('删除失败');
}
});
},
handleAssociate(row) {
this.currentRule = row;
//
this.associateSearch = '';
this.select.storehouses_id = '';
this.select.area = '';
this.associateWarehouseName = '';
this.associateCurrentPage = 1;
//
this.showAssociateModal = false;
this.$nextTick(() => {
this.showAssociateModal = true;
this.loadAssociations();
this.searchMaterialsPost();
});
},
loadAssociations() {
// Load saved associations from localStorage
const saved = localStorage.getItem('ruleAssociations');
if (saved) {
const parsed = JSON.parse(saved);
Object.entries(parsed).forEach(([ruleId, materials]) => {
this.ruleAssociations.set(ruleId, new Set(materials));
});
}
},
saveAssociations() {
const serialized = {};
this.ruleAssociations.forEach((materials, ruleId) => {
serialized[ruleId] = Array.from(materials);
});
localStorage.setItem('ruleAssociations', JSON.stringify(serialized));
},
async searchMaterialsPost() {
try {
const data = {
page_size: this.associatePageSize,
page: this.associateCurrentPage,
'show_relation[0]': 'equipmentMaintainConfig',
'show_relation[1]': 'materialInfo.materialstorage',
'materialstorages_cangkumingcheng': this.associateWarehouseName ? this.associateWarehouseName : '',
'materialstorages_suozaiquyu': this.select.area ? this.select.area : '',
'storehouses_id': this.select.storehouses_id ? this.select.storehouses_id : '',
'filter[0][key]': 'zichanmingcheng',
'filter[0][op]': 'like',
'filter[0][value]': this.associateSearch ? this.associateSearch : '',
'filter[1][key]': 'wuzileixing',
'filter[1][op]': 'eq',
'filter[1][value]': '一物一码'
};
const res = await request({
url: '/api/admin/stocks-item/index',
method: 'post',
data: qs.stringify(data),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if (res && res.data) {
this.associateList = res.data.map(material => ({
...material,
selected: this.ruleAssociations.get(this.currentRule.id)?.has(material.id) || false
}));
this.associateTotal = res.total;
}
} catch (e) {
console.error('获取防汛物资列表失败:', e);
this.$message.error('获取防汛物资列表失败');
}
},
async toggleAssociation(row) {
if (!this.currentRule) return;
if (row.equipment_maintain_config?.id === this.currentRule.id) {
//
try {
await saveStocksItem({
id: row.id,
equipment_maintain_config_id: null
});
this.$message.success(`已解除绑定物资"${row.material_info.suozaicangku}"`);
this.searchMaterialsPost();
} catch (e) {
this.$message.error('解除绑定失败');
}
} else {
//
try {
await saveStocksItem({
id: row.id,
equipment_maintain_config_id: this.currentRule.id
});
this.$message.success(`已成功绑定物资"${row.material_info.suozaicangku}"`);
this.searchMaterialsPost();
} catch (e) {
this.$message.error('绑定失败');
}
}
},
updateMaterialCount() {
const count = this.ruleAssociations.get(this.currentRule.id)?.size || 0;
const index = this.list.findIndex(item => item.id === this.currentRule.id);
if (index !== -1) {
this.$set(this.list[index], 'materialCount', count);
}
},
handleAssociatePageChange(page) {
this.associateCurrentPage = page;
this.searchMaterialsPost();
},
handleAssociateSubmit() {
this.showAssociateModal = false;
this.getList();
},
handleAssociateCancel() {
this.showAssociateModal = false;
},
handleAssociatePageSizeChange(size) {
this.associatePageSize = size;
this.associateCurrentPage = 1;
this.searchMaterialsPost();
},
resetAssociateSearch() {
this.associateSearch = '';
this.select.storehouses_id = '';
this.select.area = '';
this.associateWarehouseName = '';
this.associateCurrentPage = 1;
this.searchMaterialsPost();
},
async getWarehouseTypes() {
try {
const res = await getStorehouseTypeList({
page: 1,
page_size: 999
});
if (res && res.data) {
this.warehouseTypes = res.data;
}
} catch (e) {
console.error('获取仓库类型列表失败:', e);
this.$message.error('获取仓库类型列表失败');
}
},
async getWarehouseAreas() {
try {
const res = await getparameteritem("area");
if (res && res.detail) {
this.warehouseAreas = res.detail;
}
} catch (e) {
console.error('获取仓库区域列表失败:', e);
this.$message.error('获取仓库区域列表失败');
}
},
async getWarehouseNames() {
try {
const res = await index({
page_size: this.select.page_size,
page: this.select.page,
table_name: this.select.table_name,
filter: [{
key: 'cangkumingcheng',
op: 'like',
value: this.select.keyword ? this.select.keyword : ''
}, {
key: 'quyu_id',
op: 'eq',
value: this.select.area ? this.select.area : ''
}, {
key: 'storehouses_id',
op: 'eq',
value: this.select.storehouses_id ? this.select.storehouses_id : ''
}],
});
if (res && res.data) {
this.warehouseNames = res.data.map(warehouse => ({
value: warehouse.id,
label: warehouse.cangkumingcheng
}));
}
} catch (e) {
console.error('获取仓库名称列表失败:', e);
this.$message.error('获取仓库名称列表失败');
}
},
clearArea(e) {
this.select.area = e || '';
this.getWarehouseNames();
},
clearType(e) {
this.select.storehouses_id = e || '';
this.getWarehouseNames();
},
async getAllWarehouses() {
try {
const formData = new FormData();
formData.append('page', 1);
formData.append('page_size', 999);
formData.append('table_name', 'materialstorages');
const res = await index(formData);
if (res && res.data) {
this.allWarehouses = res.data;
this.filterWarehouses();
}
} catch (e) {
console.error('获取仓库列表失败:', e);
this.$message.error('获取仓库列表失败');
}
},
filterWarehouses() {
let filtered = [...this.allWarehouses];
//
if (this.select.storehouses_id) {
filtered = filtered.filter(warehouse =>
warehouse.storehouses_id === this.select.storehouses_id
);
}
//
if (this.select.area) {
filtered = filtered.filter(warehouse =>
warehouse.quyu_id === this.select.area
);
}
//
this.warehouseNames = filtered.map(warehouse => ({
value: warehouse.id,
label: warehouse.cangkumingcheng
}));
},
clearArea(e) {
this.select.area = e || '';
this.filterWarehouses();
},
clearType(e) {
this.select.storehouses_id = e || '';
this.filterWarehouses();
},
async handleCustomSubmit() {
this.$refs.form.validate(async (valid) => {
if (valid) {
const data = {
name: this.form.name,
month_frequency: this.form.cycle,
start_standard: this.form.startSetting,
advance_days: this.form.timingValue,
next_plan_type: this.form.nextPlan,
remark: this.form.notes,
equipment_maintain_config_stocks_items: this.selectedMaterialIds?.map(id => [ { stocks_item_id: id } ]) || []
};
try {
await saveMaintainConfig(data);
this.$message.success('保存成功');
this.showModal = false;
this.getList();
} catch (e) {
this.$message.error('保存失败');
}
} else {
this.$message.error('请填写所有必填项');
}
});
}
}
}
</script>
<style>
.table-page-container {
display: flex;
flex-direction: column;
height: 100vh;
box-sizing: border-box;
background: #f5f7fa;
}
.table-flex-content {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
}
.el-table th {
background: #f5f7fa !important;
color: #333;
font-weight: bold;
font-size: 15px;
}
.el-table td {
font-size: 14px;
height: 48px;
}
.el-table {
border-radius: 10px;
overflow: hidden;
width: 100%;
height: 100%;
}
.timing-controls {
display: flex;
align-items: center;
gap: 8px;
}
.associate-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.pagination-container {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
}
/* 确保加载提示显示在模态框之上 */
.ivu-message {
z-index: 9999 !important;
}
.ivu-loading-mask {
z-index: 9999 !important;
}
.ivu-message-notice {
z-index: 9999 !important;
}
.ivu-message-notice-content {
z-index: 9999 !important;
}
.ivu-modal-mask {
z-index: 1000 !important;
}
.ivu-modal-wrap {
z-index: 1000 !important;
}
</style>

@ -49,8 +49,8 @@
<div class="xy-table-item type-dialog-item">
<div class="xy-table-item-label">原值</div>
<div class="xy-table-item-content"><el-input-number v-model="form.original_value" style="width:100%;" :min="0" :step="0.01" :precision="2" placeholder="请输入原值" /></div>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="xy-table-item type-dialog-item">
<div class="xy-table-item-label">使用科室</div>
@ -59,8 +59,8 @@
<div class="xy-table-item type-dialog-item">
<div class="xy-table-item-label">保管人</div>
<div class="xy-table-item-content"><Input v-model="form.keeper" style="width:100%;" placeholder="请输入保管人" /></div>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="xy-table-item type-dialog-item">
<div class="xy-table-item-label">存放地点</div>
@ -70,12 +70,12 @@
<div class="xy-table-item-label">所属库</div>
<div class="xy-table-item-content"><Input v-model="form.warehouse" style="width:100%;" placeholder="请输入所属库" /></div>
</div>
</div>
</div>
<div class="form-row">
<div class="xy-table-item type-dialog-item">
<div class="xy-table-item-label">使用方向</div>
<div class="xy-table-item-content"><Input v-model="form.usage_direction" style="width:100%;" placeholder="请输入使用方向" /></div>
</div>
</div>
<div class="xy-table-item type-dialog-item">
<div class="xy-table-item-label">备注</div>
<div class="xy-table-item-content"><Input v-model="form.remark" style="width:100%;" placeholder="请输入备注" /></div>

Loading…
Cancel
Save