推送项目重构代码

This commit is contained in:
2026-05-29 19:52:32 +08:00
parent 95141d0e1f
commit 3dafaceef2
65 changed files with 3762 additions and 583 deletions

View File

@@ -1,159 +1,24 @@
<template>
<div class="home">
<el-container>
<el-main>
<el-row style="flex:1;overflow:auto; margin:16px 0;">
<QuickEntry />
</el-row>
<el-row :gutter="20">
<!-- 左侧宫格区域 -->
<el-col :span="20">
<el-row :gutter="20">
<el-col :span="12">
<el-card>
<el-tabs>
<el-tab-pane label="通知公告">
<Announcements />
</el-tab-pane>
<el-tab-pane label="问题反馈">
<FeedbackList />
</el-tab-pane>
<el-tab-pane label="需求下发">
<RequirementList />
</el-tab-pane>
<el-tab-pane label="快递问题">
<ExpressQuestionList />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<ProjectManagement />
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<el-card>
<el-tabs>
<el-tab-pane label="分配我的任务">
<OwnerTaskList />
</el-tab-pane>
<el-tab-pane label="我发放的任务">
<MyTaskList />
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
<el-col :span="12">
<FinancialCharts />
</el-col>
</el-row>
</el-col>
<!-- 右侧整体日程区域 -->
<el-col :span="4">
<div class="panel panel-calendar" style="height:100%;display:flex;flex-direction:column;">
<MiniCalendar :daysData="calendarDays" />
</div>
</el-col>
</el-row>
</el-main>
</el-container>
<Workbench />
</div>
</template>
<script>
import { getCache } from "@/api/monitor/cache";
import { queryOwnList } from "@/api/oa/oaHoliday";
import { getRemindList, updateRemind } from '@/api/oa/remind';
import { getNotice, listNoticeLimit } from "@/api/system/notice";
import Announcements from "@/components/Announcements/index.vue";
import { ExpressQuestionList, FeedbackList, FinancialCharts, MyTaskList, OwnerTaskList, ProjectManagement, RequirementList } from '@/components/HomeModules/index';
import Inventory from "@/components/Inventory/index.vue";
import MiniCalendar from "@/components/MiniCalendar/index.vue";
// import ProjectManagement from "@/components/ProjectManagement/index.vue";
import QuickAccess from "@/components/QuickAccess/index.vue";
import QuickEntry from "@/components/QuickEntry/index.vue";
import { formatDate } from "@/utils";
import { getRemindList, updateRemind } from '@/api/oa/remind'
import Workbench from '@/components/Workbench/index.vue'
export default {
name: "Index",
components: {
MiniCalendar,
FinancialCharts,
ProjectManagement,
Announcements,
Inventory,
QuickAccess,
QuickEntry,
MyTaskList,
OwnerTaskList,
ExpressQuestionList,
RequirementList,
FeedbackList
},
data () {
return {
version: "0.8.3",
commandstats: null,
usedmemory: null,
cache: [],
finishedCount: 0,
todoListCount: 0,
ownCount: 0,
noticeList: [],
noticeTitle: "",
noticeContent: "",
drawer: false,
dayParams: {
pageSize: 999,
pageNum: 1,
holidayTime: new Date(),
},
calendarDays: [],
};
},
name: 'Index',
components: { Workbench },
created () {
this.getList();
this.getListNotice();
this.checkTaskRemind(); // 新增
this.checkTaskRemind()
},
methods: {
getList () {
this.loading = true;
getCache().then((response) => {
this.cache = response.data;
// this.$modal.closeLoading();
});
this.dayParams.holidayTime = formatDate(new Date());
queryOwnList(this.dayParams).then((response) => {
this.calendarDays = response;
this.loading = false;
});
},
getListNotice () {
this.loading = true;
listNoticeLimit().then((response) => {
this.noticeList = response;
this.loading = false;
});
},
toDrawer (nid) {
this.drawer = true;
getNotice(nid).then((res) => {
this.noticeTitle = res.data.noticeTitle;
this.noticeContent = res.data.noticeContent;
});
},
goTarget (href) {
this.$router.push({ path: href });
},
checkTaskRemind () {
const targetUserId = this.$store.getters.id
getRemindList({ pageNum: 1, pageSize: 20, targetUserId }).then(res => {
const taskReminds = (res.data || []).filter(item => item.remindType === 'task');
console.log(taskReminds, res)
const taskReminds = (res.data || []).filter(item => item.remindType === 'task')
taskReminds.forEach(remind => {
const notifyInstance = this.$notify({
title: '任务提醒',
@@ -165,24 +30,21 @@ export default {
dangerouslyUseHTMLString: true,
duration: 0,
showClose: true,
customClass: 'remind-notify',
onClose: () => { },
onClick: () => { },
});
customClass: 'remind-notify'
})
this.$nextTick(() => {
const btn = document.getElementById(`remind-btn-${remind.remindId}`);
const btn = document.getElementById(`remind-btn-${remind.remindId}`)
if (btn) {
btn.addEventListener('click', (e) => {
e.stopPropagation();
notifyInstance.close();
this.$router.push({ path: '/task/task', query: { taskId: remind.detailId } });
});
e.stopPropagation()
notifyInstance.close()
this.$router.push({ path: '/task/task', query: { taskId: remind.detailId } })
})
}
});
});
})
})
// 专属于董事长的薪资待批
const gmReminds = (res.data || []).filter(item => item.remindType === 'salary');
const gmReminds = (res.data || []).filter(item => item.remindType === 'salary')
gmReminds.forEach(remind => {
const notifyInstance = this.$notify({
title: '薪资待批',
@@ -194,83 +56,35 @@ export default {
dangerouslyUseHTMLString: true,
duration: 0,
showClose: true,
customClass: 'remind-notify',
onClose: () => { },
onClick: () => { },
});
customClass: 'remind-notify'
})
this.$nextTick(() => {
const btn = document.getElementById(`remind-btn-${remind.remindId}`);
const btn = document.getElementById(`remind-btn-${remind.remindId}`)
if (btn) {
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.stopPropagation()
updateRemind({
...remind,
taskTime: this.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
taskStatus: 1,
taskStatus: 1
})
notifyInstance.close();
this.$router.push({ path: '/finance/salary/list' });
});
notifyInstance.close()
this.$router.push({ path: '/finance/salary/list' })
})
}
});
});
});
},
},
};
})
})
})
}
}
}
</script>
<style scoped lang="scss">
.onboarding-homepage {
max-width: 800px;
margin: 20px auto;
padding: 20px;
.home {
height: 100%;
}
.employee-form {
margin-top: 20px;
margin-bottom: 30px;
}
.flow-chart {
margin-top: 30px;
}
/* 栅格列样式 */
.content-area {
background: #fff;
}
/* 左侧功能区域 */
.sidebar {
background: #f9f9f9;
}
/* 响应式隐藏侧边栏 */
@media screen and (max-width: 768px) {
.sidebar {
display: none;
}
.content-area {
width: 100% !important;
}
}
.remind-notify {
cursor: pointer;
}
/* 固定卡片高度 */
.el-card {
height: 430px;
display: flex;
flex-direction: column;
overflow-y: scroll;
}
.el-card__body {
flex: 1 1 auto;
overflow: auto;
}
</style>

View File

@@ -26,21 +26,36 @@
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" />
<el-table-column label="入库单编号" align="center" prop="masterNum" />
<el-table-column label="操作时间" align="center" prop="signTime">
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange" stripe size="small">
<el-table-column type="selection" width="40" align="center" />
<el-table-column label="入库时间" prop="signTime" width="100">
<template slot-scope="scope">{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</template>
</el-table-column>
<el-table-column label="入库人" prop="signUser" width="80" />
<el-table-column label="关联需求" min-width="160" show-overflow-tooltip>
<template slot-scope="scope">
<span>{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</span>
<span v-if="scope.row.requirementName">{{ scope.row.requirementName }}</span>
<span v-else style="color:#c0c4cc;">无关联</span>
</template>
</el-table-column>
<el-table-column label="操作人" align="center" prop="signUser" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="关联项目" prop="projectName" min-width="160" show-overflow-tooltip>
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">查看详情
</el-button>
<span v-if="scope.row.projectName">{{ scope.row.projectName }}</span>
<span v-else style="color:#c0c4cc;"></span>
</template>
</el-table-column>
<el-table-column label="物料概览" prop="itemsSummary" min-width="220" show-overflow-tooltip>
<template slot-scope="scope">
<span v-if="scope.row.itemsSummary">{{ scope.row.itemsSummary }}</span>
<el-button v-else type="text" size="mini" @click="showDetail(scope.row)">查看</el-button>
</template>
</el-table-column>
<el-table-column label="物料种类" prop="itemCount" width="80" align="right" />
<el-table-column label="入库总数" prop="totalQty" width="80" align="right" />
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="100" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
@@ -58,7 +73,7 @@
<i class="el-icon-s-order"></i>
入库单
</template>
{{ detailData.masterId }}
{{ detailData.masterNum }}
</el-descriptions-item>
<el-descriptions-item>
@@ -106,6 +121,20 @@
<!-- 添加或修改仓库入库对话框 -->
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="采购需求" prop="requirementId" required>
<el-select v-model="form.requirementId" filterable remote
:remote-method="loadRequirementOptions" :loading="requirementLoading"
placeholder="按需求标题搜索(必选)" style="width: 100%"
@change="onRequirementChange">
<el-option v-for="r in requirementOptions" :key="r.requirementId"
:label="r.title" :value="r.requirementId">
<span style="float: left">{{ r.title }}</span>
<span style="float: right; color: #8492a6; font-size: 12px">
{{ r.projectName || '无项目' }}
</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="绑定项目">
<el-radio-group v-model="projectFlag" :disabled="drawer">
<el-radio :label="true"></el-radio>
@@ -216,6 +245,7 @@ import {
listOaWarehouseMaster,
updateOaWarehouseMaster
} from "@/api/oa/warehouse/warehouseMaster";
import { listRequirements } from "@/api/oa/requirement";
import ProjectSelect from "@/components/fad-service/ProjectSelect";
export default {
@@ -270,6 +300,9 @@ export default {
open: false,
// 是否绑定项目
projectFlag: false,
// 采购需求下拉
requirementOptions: [],
requirementLoading: false,
// 库存查询参数
warehouseParams: {
pageSize: 999,
@@ -297,6 +330,9 @@ export default {
warehouseId: [
{ required: true, message: "入库对象id不能为空", trigger: "blur" }
],
requirementId: [
{ required: true, message: "入库必须关联采购需求", trigger: "change" }
],
}
};
},
@@ -304,6 +340,20 @@ export default {
this.getList();
},
methods: {
// 远程搜索采购需求
loadRequirementOptions (keyword) {
this.requirementLoading = true
listRequirements({ pageNum: 1, pageSize: 20, title: keyword || undefined, status: 1 })
.then(res => { this.requirementOptions = res.rows || [] })
.finally(() => { this.requirementLoading = false })
},
onRequirementChange (id) {
const r = this.requirementOptions.find(x => x.requirementId === id)
if (r && r.projectId) {
this.projectFlag = true
this.form.projectId = r.projectId
}
},
// 添加新的一行
addRow () {
@@ -358,6 +408,7 @@ export default {
reset () {
this.form = {
projectId: undefined,
requirementId: undefined,
warehouseList: [],
};
this.resetForm("form");
@@ -382,6 +433,7 @@ export default {
handleAdd () {
this.reset();
this.open = true;
this.loadRequirementOptions();
if (this.drawer) {
// 如果抽屉是打开的说明是从项目处进入的新增从而加入projectId
this.projectFlag = true;

View File

@@ -0,0 +1,102 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="物料名" prop="warehouseName">
<el-input v-model="queryParams.warehouseName" placeholder="名称模糊匹配" clearable style="width: 180px" />
</el-form-item>
<el-form-item label="项目" prop="projectId">
<project-select v-model="queryParams.projectId" style="width: 200px" />
</el-form-item>
<el-form-item label="出库时间" prop="signTimeRange">
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始" end-placeholder="结束"
value-format="yyyy-MM-dd" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" stripe size="small">
<el-table-column label="出库时间" prop="signTime" width="120">
<template slot-scope="scope">{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</template>
</el-table-column>
<el-table-column label="物料" prop="warehouseName" min-width="160" show-overflow-tooltip />
<el-table-column label="型号" prop="model" min-width="120" show-overflow-tooltip />
<el-table-column label="规格" prop="specifications" min-width="120" show-overflow-tooltip />
<el-table-column label="出库数量" prop="amount" width="90" align="right" />
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="单价" prop="signPrice" width="90" align="right">
<template slot-scope="scope">¥{{ Number(scope.row.signPrice || 0).toFixed(2) }}</template>
</el-table-column>
<el-table-column label="小计" width="100" align="right">
<template slot-scope="scope">
¥{{ ((scope.row.signPrice || 0) * (scope.row.amount || 0)).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="项目" prop="projectName" min-width="160" show-overflow-tooltip />
<el-table-column label="出库单" prop="masterNum" width="140" show-overflow-tooltip />
<el-table-column label="操作人" prop="signUser" width="90" />
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
</el-table>
<pagination v-show="total > 0" :total="total"
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
</div>
</template>
<script>
import { listOaWarehouseMaster } from '@/api/oa/warehouse/warehouseMaster'
import request from '@/utils/request'
import ProjectSelect from '@/components/fad-service/ProjectSelect'
export default {
name: 'OutWarehouseDetail',
components: { ProjectSelect },
data () {
return {
loading: false,
list: [],
total: 0,
searchTime: [],
queryParams: {
pageNum: 1,
pageSize: 20,
type: 0,
warehouseName: undefined,
projectId: undefined,
signTimeStart: undefined,
signTimeEnd: undefined
}
}
},
created () { this.getList() },
watch: {
searchTime (v) {
this.queryParams.signTimeStart = v && v[0] || undefined
this.queryParams.signTimeEnd = v && v[1] || undefined
}
},
methods: {
getList () {
this.loading = true
// 走 detail 列表接口type=0 出库
request({
url: '/oa/oaOutWarehouse/list',
method: 'get',
params: this.queryParams
}).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleQuery () { this.queryParams.pageNum = 1; this.getList() },
resetQuery () {
this.searchTime = []
this.queryParams = { pageNum: 1, pageSize: 20, type: 0 }
this.getList()
}
}
}
</script>

View File

@@ -1,36 +1,31 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="操作时间" prop="signTime">
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']">
</el-date-picker>
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
label-width="62px" class="compact-search">
<el-form-item label="项目" prop="projectId">
<project-select v-model="queryParams.projectId" style="width: 180px" />
</el-form-item>
<el-form-item label="出库单" prop="masterNum">
<el-input v-model="queryParams.masterNum" placeholder="单号" clearable style="width: 140px"
@keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="时间" prop="signTime">
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始" end-placeholder="结束"
:default-time="['00:00:00', '23:59:59']" style="width: 230px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-alert title="这个页面的退库按钮只是让他显示在退库管理中,并不会修改库存数量,在退库页面点击退库后才变更库存" type="warning" show-icon />
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增
</el-button>
</el-col>
<el-col :span="1.5">
<el-form-item class="action-group">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
@click="handleDeleteMaster">删除
</el-button>
</el-col>
<el-col :span="1.5">
@click="handleDeleteMaster">删除</el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport"
v-hasPermi="['oa:oaOutWarehouse:export']">导出
</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
v-hasPermi="['oa:oaOutWarehouse:export']">导出</el-button>
</el-form-item>
</el-form>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
@@ -58,7 +53,7 @@
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-refresh-right" @click="toRedo(scope.row)">撤销出库
</el-button>
<el-button size="mini" type="text" icon="el-icon-back" @click="toReturn(scope.row)" title="将本条记录标记为需要退库">退库
<el-button size="mini" type="text" icon="el-icon-back" @click="openReturnDialog(scope.row)" title="直接退库到库存">退库
</el-button>
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">查看详情
</el-button>
@@ -79,7 +74,7 @@
<i class="el-icon-s-order"></i>
出库单
</template>
{{ detailData.masterId }}
{{ detailData.masterNum }}
</el-descriptions-item>
<el-descriptions-item>
@@ -115,18 +110,25 @@
</el-descriptions-item>
</el-descriptions>
<el-table v-loading="loading" :data="oaOutWarehouseList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" />
<el-table v-loading="loading" :data="oaOutWarehouseList">
<el-table-column label="序号" align="center" type="index" width="60" />
<el-table-column label="物料名" align="center" prop="warehouseName" />
<el-table-column label="出库数量" align="center" prop="amount" />
<el-table-column label="出库价格" align="center" prop="signPrice" />
<el-table-column label="出库数量" align="center" prop="amount" width="90" />
<el-table-column label="出库价格" align="center" prop="signPrice" width="100" />
<el-table-column label="型号" align="center" prop="model" />
<el-table-column label="规格" align="center" prop="specifications" />
<el-table-column label="品牌" align="center" prop="brand" />
<el-table-column v-if="returnMode" label="退库数量" align="center" width="130">
<template slot-scope="scope">
<el-input v-model.number="scope.row.returnAmount" type="number" size="mini" :min="0"
:max="scope.row.amount" placeholder="0" @input="onReturnAmountInput($event, scope.row)" />
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="detail = false">关闭</el-button>
<el-button v-if="returnMode" type="primary" icon="el-icon-refresh-left"
:disabled="!hasReturnItems()" @click="submitReturn">确认退库</el-button>
</div>
</el-dialog>
@@ -240,7 +242,7 @@
<script>
import {
delOaOutWarehouse, updateReturnType
delOaOutWarehouse, redoDetail, updateReturnType
} from "@/api/oa/warehouse/oaOutWarehouse";
import { listByMultiQuery, listOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
import {
@@ -259,6 +261,8 @@ export default {
},
data () {
return {
// 退库模式(弹窗里显示数量输入和确认按钮)
returnMode: false,
// 细节数据
detailData: {},
// 抽屉
@@ -312,7 +316,7 @@ export default {
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 50,
type: 0,
},
// 表单参数
@@ -367,6 +371,37 @@ export default {
.catch(() => { });
},
// 就地打开退库弹窗,输入数量后直接调 redoDetail 扣明细 + 回库存
openReturnDialog (row) {
this.returnMode = true
this.detailData = row
this.oaOutWarehouseList = (row.warehouseList || []).map(it => ({ ...it, returnAmount: '' }))
this.detail = true
},
onReturnAmountInput (val, row) {
if (val === '') { row.returnAmount = ''; return }
const n = Number(val)
if (isNaN(n) || n < 0) { row.returnAmount = ''; return }
if (n > row.amount) { row.returnAmount = row.amount; this.$message.warning('不能超过出库数量') }
else row.returnAmount = Math.floor(n)
},
hasReturnItems () {
return this.oaOutWarehouseList.some(it => Number(it.returnAmount) > 0)
},
submitReturn () {
const items = this.oaOutWarehouseList.filter(it => Number(it.returnAmount) > 0)
if (!items.length) return
const summary = items.map(it => `${it.warehouseName}×${it.returnAmount}`).join('、')
this.$confirm(`确认退库:${summary} `, '退库确认', { type: 'warning' }).then(() => {
const payload = items.map(it => ({ detailId: it.id, returnNum: Number(it.returnAmount) }))
redoDetail(payload).then(() => {
this.$modal.msgSuccess('退库成功')
this.detail = false
this.returnMode = false
this.getList()
})
}).catch(() => {})
},
toReturn (row) {
this.$confirm("确定要对此出库单进行退货操作吗?", "提示", {
confirmButtonText: "确定",
@@ -726,3 +761,20 @@ export default {
}
};
</script>
<style lang="scss" scoped>
/* 搜索 / 重置 之后再隔一段空白接 新增/删除/导出 */
::v-deep .action-group {
margin-left: 16px;
position: relative;
&::before {
content: '';
position: absolute;
left: -8px;
top: 4px;
bottom: 4px;
width: 1px;
background: #e4e7ed;
}
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
label-width="68px" class="compact-search">
<el-form-item label="操作时间" prop="signTime">
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']">
@@ -12,13 +13,9 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="info" plain icon="el-icon-document" size="mini" @click="viewReturnLog">退库日志
</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<el-button type="text" icon="el-icon-document" size="mini" class="view-log-link"
@click="viewReturnLog">查看退库日志</el-button>
<el-table v-loading="loading" :data="outWareHouseList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
@@ -70,7 +67,7 @@
<i class="el-icon-s-order"></i>
出库单
</template>
{{ detailData.masterId }}
{{ detailData.masterNum }}
</el-descriptions-item>
<el-descriptions-item>
@@ -172,7 +169,7 @@ export default {
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 50,
type: 0,
returnType: 1
}
@@ -382,3 +379,15 @@ export default {
}
};
</script>
<style lang="scss" scoped>
.view-log-link {
position: absolute;
top: 14px;
right: 110px;
color: #909399;
font-size: 11px;
z-index: 3;
&:hover { color: #409eff; }
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
<el-form-item label="操作类型" prop="opType">
<el-select v-model="queryParams.opType" placeholder="全部" clearable style="width: 160px">
<el-option v-for="t in opTypes" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
</el-form-item>
<el-form-item label="实体类型" prop="refType">
<el-select v-model="queryParams.refType" placeholder="全部" clearable style="width: 140px">
<el-option label="采购需求" value="requirement" />
<el-option label="车间采购" value="task" />
<el-option label="出入库单" value="master" />
<el-option label="物料" value="warehouse" />
</el-select>
</el-form-item>
<el-form-item label="实体ID" prop="refId">
<el-input v-model="queryParams.refId" placeholder="可选" clearable style="width: 160px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="list" stripe size="small">
<el-table-column label="时间" prop="opTime" width="160">
<template slot-scope="scope">{{ parseTime(scope.row.opTime, '{y}-{m}-{d} {h}:{i}') }}</template>
</el-table-column>
<el-table-column label="操作人" prop="opUserName" width="100" />
<el-table-column label="类型" prop="opType" width="120">
<template slot-scope="scope">
<el-tag :type="tagType(scope.row.opType)" size="mini">{{ tagLabel(scope.row.opType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="摘要" prop="summary" min-width="320" show-overflow-tooltip />
<el-table-column label="关联" width="180">
<template slot-scope="scope">
<span v-if="scope.row.refType">{{ refLabel(scope.row.refType) }} #{{ scope.row.refId }}</span>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total"
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
</div>
</template>
<script>
import { listWarehouseAudit } from '@/api/oa/warehouse/auditLog'
const TAG_MAP = {
REQ_CREATE: { type: 'success', label: '需求新建' },
REQ_UPDATE: { type: 'info', label: '需求修改' },
REQ_DONE: { type: 'success', label: '需求完成' },
REQ_CANCEL: { type: 'warning', label: '需求取消' },
REQ_DELETE: { type: 'danger', label: '需求删除' },
TASK_CREATE: { type: 'success', label: '采购创建' },
TASK_DONE: { type: 'success', label: '采购完成' },
TASK_CANCEL: { type: 'warning', label: '采购取消' },
TASK_UPDATE: { type: 'info', label: '采购修改' },
IN: { type: 'success', label: '入库' },
OUT: { type: 'warning', label: '出库' },
RETURN: { type: 'info', label: '退库' },
STOCK_ADJUST:{ type: 'danger', label: '库存修正' }
}
export default {
name: 'WarehouseAuditLog',
data () {
return {
loading: false,
list: [],
total: 0,
queryParams: { pageNum: 1, pageSize: 20, opType: undefined, refType: undefined, refId: undefined },
opTypes: Object.keys(TAG_MAP).map(k => ({ value: k, label: TAG_MAP[k].label }))
}
},
created () { this.getList() },
methods: {
getList () {
this.loading = true
listWarehouseAudit(this.queryParams).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
handleQuery () { this.queryParams.pageNum = 1; this.getList() },
resetQuery () {
this.queryParams = { pageNum: 1, pageSize: 20, opType: undefined, refType: undefined, refId: undefined }
this.getList()
},
tagType (op) { return (TAG_MAP[op] || {}).type || '' },
tagLabel (op) { return (TAG_MAP[op] || {}).label || op },
refLabel (ref) {
return { requirement: '采购需求', task: '车间采购', master: '出入库单', warehouse: '物料' }[ref] || ref
}
}
}
</script>

View File

@@ -0,0 +1,472 @@
<template>
<el-dialog title="新建采购单" :visible.sync="visibleProxy" width="1080px" append-to-body
:close-on-click-modal="false" custom-class="add-purchase-dialog" @close="handleClose">
<!-- 选中清单 sticky strip任何步骤都看得到 -->
<div class="selected-strip" v-if="selected.length">
<span class="strip-label">已选 {{ selected.length }} </span>
<el-tag v-for="(it, idx) in selected" :key="it._key" type="info" size="small"
closable disable-transitions @close="removeItem(idx)" class="strip-tag">
{{ tagText(it) }}
</el-tag>
</div>
<div v-else class="selected-strip empty">还没选任何物料</div>
<!-- 步骤指示 -->
<el-steps :active="step" simple class="compact-steps" finish-status="success">
<el-step title="选物料" icon="el-icon-search" />
<el-step title="设数量备注" icon="el-icon-edit-outline" />
<el-step title="确认提交" icon="el-icon-check" />
</el-steps>
<!-- Step 1: 选物料 + 右侧推荐 -->
<div v-if="step === 0" class="step1-wrap">
<div class="step1-main">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
<el-input v-model="keyword" placeholder="搜索 物料名 / 型号 / 品牌" clearable
size="mini" prefix-icon="el-icon-search" style="flex:1"
@input="onSearch" @clear="onSearch" />
<el-button size="mini" type="success" plain icon="el-icon-plus"
@click="newMatVisible = true">新增物料</el-button>
</div>
<!-- 新增物料子弹窗 -->
<el-dialog title="新增物料" :visible.sync="newMatVisible" width="520px" append-to-body
:close-on-click-modal="false">
<el-form ref="newMatForm" :model="newMat" :rules="newMatRules" size="mini" label-width="80px">
<el-form-item label="物料名" prop="name">
<el-input v-model="newMat.name" placeholder="必填RVV 电源线" />
</el-form-item>
<el-form-item label="型号" prop="model">
<el-input v-model="newMat.model" placeholder="如RVV-2×1.5" />
</el-form-item>
<el-form-item label="品牌" prop="brand">
<el-input v-model="newMat.brand" placeholder="如:远东" />
</el-form-item>
<el-form-item label="规格" prop="specifications">
<el-input v-model="newMat.specifications" placeholder="如100m/卷" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="newMat.unit" placeholder="如:卷/个/m" />
</el-form-item>
<el-form-item label="预警阈值" prop="threshold">
<el-input-number v-model="newMat.threshold" :min="0" controls-position="right"
style="width: 140px" />
<span style="color:#909399; margin-left:8px;">库存低于此值会提醒补货</span>
</el-form-item>
<el-form-item label="本次采购" prop="taskInventory">
<el-input-number v-model="newMat.taskInventory" :min="1" controls-position="right"
style="width: 140px" />
</el-form-item>
</el-form>
<span slot="footer">
<el-button size="mini" @click="newMatVisible = false">取消</el-button>
<el-button size="mini" type="primary" :loading="newMatSubmitting"
@click="submitNewMaterial">保存并加入采购单</el-button>
</span>
</el-dialog>
<el-table v-loading="loading" :data="list" size="mini" stripe
:row-class-name="rowDangerClass" max-height="320">
<template slot="empty">
<div style="padding: 24px 0; text-align: center;">
<div style="color:#909399; margin-bottom: 12px;">
<i class="el-icon-search" style="font-size: 28px;"></i>
<div style="margin-top: 6px;">没有找到匹配的物料</div>
<div v-if="keyword" style="font-size: 11px; margin-top: 4px;">
库里没有{{ keyword }}直接新增
</div>
</div>
<el-button type="success" size="mini" icon="el-icon-plus"
@click="openNewMatFromSearch">新增物料</el-button>
</div>
</template>
<el-table-column label="物料" prop="name" min-width="120" show-overflow-tooltip />
<el-table-column label="型号" prop="model" min-width="100" show-overflow-tooltip />
<el-table-column label="品牌" prop="brand" min-width="100" show-overflow-tooltip />
<el-table-column label="规格" prop="specifications" min-width="100" show-overflow-tooltip />
<el-table-column label="库存" prop="inventory" width="64" align="right" />
<el-table-column label="阈值" prop="threshold" width="64" align="right" />
<el-table-column label="操作" width="56" align="center">
<template slot-scope="s">
<el-button v-if="!isSelected(s.row)" type="text" class="add-mini-btn"
icon="el-icon-circle-plus-outline" @click="addItem(s.row)">加入</el-button>
<i v-else class="el-icon-check" style="color:#67c23a; font-size:14px;" title="已加" />
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" :page-sizes="[20, 50, 100]" @pagination="loadList" />
</div>
<!-- 右侧 推荐低于阈值 Top 20按库存与阈值差倒序 -->
<div class="step1-side">
<div class="side-header">
<i class="el-icon-magic-stick" style="color:#e6a23c;"></i>
<span>推荐补货{{ recommend.length }}</span>
</div>
<div class="side-list" v-loading="recommendLoading">
<div v-for="r in recommend" :key="r.id" class="rec-item" :class="{ disabled: recIsSelected(r) }"
@click="recIsSelected(r) ? null : addRecommend(r)">
<div class="rec-text" :title="recommendText(r)">
{{ recommendText(r) }}
</div>
<div class="rec-meta">
<span class="rec-stock">库存 {{ r.inventory || 0 }} / 阈值 {{ r.threshold || 0 }}</span>
<i v-if="recIsSelected(r)" class="el-icon-check" style="color:#67c23a"></i>
<i v-else class="el-icon-circle-plus-outline" style="color:#409eff"></i>
</div>
</div>
<div v-if="!recommendLoading && !recommend.length" class="rec-empty">
暂无低于阈值的物料
</div>
</div>
</div>
</div>
<!-- Step 2: 设数量备注 -->
<div v-if="step === 1">
<el-alert v-if="!selected.length" type="warning" :closable="false"
title="请先在步骤 1 选物料" />
<el-table v-else :data="selected" size="mini" stripe max-height="380">
<el-table-column label="物料" min-width="200">
<template slot-scope="s">{{ tagText(s.row) }}</template>
</el-table-column>
<el-table-column label="单位" prop="unit" width="60" />
<el-table-column label="采购数量" width="120" align="center">
<template slot-scope="s">
<el-input-number v-model="s.row.taskInventory" :min="1" size="mini" controls-position="right"
style="width: 100px" />
</template>
</el-table-column>
<el-table-column label="截止日期" width="160" align="center">
<template slot-scope="s">
<el-date-picker v-model="s.row.endTime" type="date" size="mini" value-format="yyyy-MM-dd"
placeholder="选日期" style="width: 140px" />
</template>
</el-table-column>
<el-table-column label="单项备注" min-width="160">
<template slot-scope="s">
<el-input v-model="s.row.remark" size="mini" placeholder="可不填" />
</template>
</el-table-column>
<el-table-column label="" width="50" align="center">
<template slot-scope="s">
<el-button type="text" style="color:#f56c6c" @click="removeItem(s.$index)">×</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- Step 3: 确认 -->
<div v-if="step === 2">
<el-form size="mini" label-width="80px">
<el-form-item label="关联需求">
<el-select v-model="form.requirementId" placeholder="可选" filterable clearable style="width: 100%">
<el-option v-for="r in requirementOptions" :key="r.requirementId" :label="r.title"
:value="r.requirementId" />
</el-select>
</el-form-item>
<el-form-item label="单据备注">
<el-input v-model="form.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
placeholder="例如:急、月度补货" />
<div style="color:#909399; font-size:11px;">提示备注里写"急"会被识别为加急单</div>
</el-form-item>
</el-form>
<div style="border:1px solid #ebeef5; border-radius:4px; padding:6px 10px; max-height:180px; overflow:auto;">
<div v-for="(it, i) in selected" :key="it._key"
style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dashed #f0f0f0;">
<span>{{ i + 1 }}. {{ tagText(it) }}</span>
<span style="color:#606266;">× {{ it.taskInventory }} {{ it.unit || '' }}</span>
</div>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="mini" @click="visibleProxy = false">取消</el-button>
<el-button size="mini" v-if="step > 0" @click="step--">上一步</el-button>
<el-button size="mini" type="primary" v-if="step < 2"
:disabled="step === 0 && !selected.length" @click="step++">下一步</el-button>
<el-button size="mini" type="success" v-if="step === 2"
:loading="submitting" @click="submit">提交采购单</el-button>
</span>
</el-dialog>
</template>
<script>
import { addOaWarehouse, listNotThreshold, listThreshold } from '@/api/oa/warehouse/oaWarehouse'
import { addOaWarehouseTaskBatch } from '@/api/oa/warehouse/warehouseTask'
import { listRequirements } from '@/api/oa/requirement'
export default {
name: 'AddPurchaseDialog',
props: { visible: Boolean },
data () {
return {
step: 0,
loading: false,
submitting: false,
keyword: '',
queryParams: { pageNum: 1, pageSize: 20, name: '' },
list: [],
total: 0,
// 右侧推荐
recommend: [],
recommendLoading: false,
selected: [],
form: { requirementId: undefined, remark: '' },
requirementOptions: [],
// 新增物料子弹窗
newMatVisible: false,
newMatSubmitting: false,
newMat: { name: '', model: '', brand: '', specifications: '', unit: '',
threshold: 5, taskInventory: 1 },
newMatRules: {
name: [{ required: true, message: '物料名必填', trigger: 'blur' }],
taskInventory: [{ required: true, message: '采购数量必填', trigger: 'change' }]
}
}
},
computed: {
visibleProxy: {
get () { return this.visible },
set (v) { this.$emit('update:visible', v) }
}
},
watch: {
visible (v) {
if (v) {
this.step = 0
this.selected = []
this.form = { requirementId: undefined, remark: '' }
this.keyword = ''
this.queryParams = { pageNum: 1, pageSize: 20, name: '' }
this.loadList()
this.loadRecommend()
this.loadRequirements()
}
}
},
methods: {
// 列表为空时点新增物料:把搜索词预填到物料名
openNewMatFromSearch () {
if (this.keyword) this.newMat.name = this.keyword
this.newMatVisible = true
},
submitNewMaterial () {
this.$refs.newMatForm.validate(ok => {
if (!ok) return
this.newMatSubmitting = true
const matPayload = {
name: this.newMat.name,
model: this.newMat.model,
brand: this.newMat.brand,
specifications: this.newMat.specifications,
unit: this.newMat.unit,
threshold: this.newMat.threshold,
inventory: 0,
price: 0
}
addOaWarehouse(matPayload).then(res => {
const newId = (res && res.data && (res.data.id || res.data.warehouseId)) || res.id || res.warehouseId
this.selected.push({
_key: 'new_' + Date.now(),
warehouseId: newId,
name: this.newMat.name,
model: this.newMat.model,
brand: this.newMat.brand,
specifications: this.newMat.specifications,
unit: this.newMat.unit,
taskInventory: this.newMat.taskInventory,
endTime: '',
remark: '新增物料'
})
this.$modal.msgSuccess('物料已建档并加入采购单')
this.newMatVisible = false
this.newMat = { name: '', model: '', brand: '', specifications: '', unit: '',
threshold: 5, taskInventory: 1 }
this.loadList()
}).finally(() => { this.newMatSubmitting = false })
})
},
tagText (it) {
return [it.name, it.model, it.brand, it.specifications].filter(Boolean).join(' · ')
},
onSearch () {
this.queryParams.name = this.keyword || ''
this.queryParams.pageNum = 1
this.loadList()
},
loadList () {
this.loading = true
// 统一查所有物料;低于阈值的行会被 rowDangerClass 自动染红
listNotThreshold(this.queryParams).then(res => {
this.list = res.rows || []
this.total = res.total || 0
}).finally(() => { this.loading = false })
},
loadRecommend () {
this.recommendLoading = true
listThreshold({ pageNum: 1, pageSize: 20 }).then(res => {
this.recommend = res.rows || []
}).finally(() => { this.recommendLoading = false })
},
recommendText (r) {
return [r.name, r.model, r.brand, r.specifications].filter(Boolean).join('-')
},
recIsSelected (r) {
return this.selected.some(s => s.warehouseId === r.id)
},
addRecommend (r) {
this.selected.push({
_key: r.id + '_' + Date.now(),
warehouseId: r.id,
name: r.name,
model: r.model,
brand: r.brand,
specifications: r.specifications,
unit: r.unit,
taskInventory: Math.max(1, (r.threshold || 1) - (r.inventory || 0)),
endTime: '',
remark: ''
})
},
loadRequirements () {
listRequirements({ pageNum: 1, pageSize: 200, status: 1 }).then(res => {
this.requirementOptions = res.rows || []
})
},
isSelected (row) {
return this.selected.some(s => s.warehouseId === row.id)
},
addItem (row) {
const item = {
_key: row.id + '_' + Date.now(),
warehouseId: row.id,
name: row.name,
model: row.model,
brand: row.brand,
specifications: row.specifications,
unit: row.unit,
taskInventory: Math.max(1, (row.threshold || 1) - (row.inventory || 0)),
endTime: '',
remark: ''
}
this.selected.push(item)
},
removeItem (idx) {
this.selected.splice(idx, 1)
},
rowDangerClass ({ row }) {
return (row.inventory != null && row.threshold != null && row.inventory < row.threshold)
? 'row-urgent' : ''
},
submit () {
if (!this.selected.length) {
this.$modal.msgWarning('请先选择物料')
return
}
// 把单据层 remark 透传到每一项;后端按需消化
const payload = this.selected.map(s => ({
warehouseId: s.warehouseId,
name: s.name,
model: s.model,
brand: s.brand,
specifications: s.specifications,
unit: s.unit,
taskInventory: s.taskInventory,
endTime: s.endTime,
remark: s.remark || this.form.remark || '',
requirementId: this.form.requirementId
}))
this.submitting = true
addOaWarehouseTaskBatch(payload).then(() => {
this.$modal.msgSuccess('采购单已创建')
this.visibleProxy = false
this.$emit('saved')
}).finally(() => { this.submitting = false })
},
handleClose () { this.step = 0 }
}
}
</script>
<style lang="scss" scoped>
.add-purchase-dialog ::v-deep .el-dialog__body { padding: 12px 18px; }
.selected-strip {
background: #f4f7fa;
border: 1px dashed #d8dce5;
border-radius: 4px;
padding: 6px 8px;
margin-bottom: 8px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 6px;
min-height: 32px;
font-size: 12px;
&.empty { color: #909399; }
}
.strip-label { color: #606266; margin-right: 4px; }
.strip-tag { margin-right: 0 !important; }
.compact-steps { margin-bottom: 10px; }
.compact-steps ::v-deep .el-step__title { font-size: 12px; line-height: 28px; }
.compact-steps ::v-deep .is-simple .el-step__head { font-size: 13px; }
.add-mini-btn.el-button {
padding: 0 !important;
font-size: 11px !important;
height: 20px;
line-height: 20px;
i { font-size: 12px; margin-right: 2px; }
}
/* Step1 左右布局 */
.step1-wrap { display: flex; gap: 12px; }
.step1-main { flex: 1; min-width: 0; }
.step1-side {
width: 280px;
flex-shrink: 0;
border: 1px solid #ebeef5;
border-radius: 4px;
display: flex;
flex-direction: column;
max-height: 460px;
background: #fffefb;
}
.side-header {
padding: 8px 10px;
border-bottom: 1px solid #f0f0f0;
font-weight: 600;
font-size: 12px;
background: #fdf6ec;
display: flex;
align-items: center;
gap: 6px;
}
.side-list { flex: 1; overflow-y: auto; padding: 4px; }
.rec-item {
padding: 6px 8px;
border-radius: 3px;
cursor: pointer;
margin-bottom: 4px;
font-size: 11px;
transition: all .15s;
border: 1px solid transparent;
&:hover { background: #ecf5ff; border-color: #d9ecff; }
&.disabled { opacity: .55; cursor: default; background: #f4f4f5; }
}
.rec-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #303133;
margin-bottom: 2px;
}
.rec-meta {
display: flex;
justify-content: space-between;
align-items: center;
color: #909399;
font-size: 10px;
}
.rec-stock { color: #f56c6c; }
.rec-empty { padding: 24px 0; text-align: center; color: #909399; font-size: 12px; }
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div class="app-container" v-loading="loading">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
label-width="68px" class="compact-search">
<el-form-item label="型号" prop="model">
<el-input
v-model="queryParams.model"
@@ -87,16 +88,17 @@
<el-col :span="1.5">
<el-button
plain
icon="el-icon-upload"
type="primary"
icon="el-icon-shopping-cart-2"
size="mini"
@click="addWarehouseTask"
>添加采购计划</el-button>
@click="addDialogVisible = true"
>新建采购单</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<add-purchase-dialog :visible.sync="addDialogVisible" @saved="getList" />
<el-table v-loading="loading" :data="oaWarehouseList" @selection-change="handleSelectionChange" :row-class-name="tableRowClassName">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index"/>
@@ -227,10 +229,13 @@
import { delOaWarehouse, getOaWarehouse, listOaWarehouse, updateOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
import { addOaWarehouseMasterToIn } from "@/api/oa/warehouse/warehouseMaster";
import { getToken } from "@/utils/auth";
import AddPurchaseDialog from "./components/AddPurchaseDialog.vue";
export default {
name: "OaWarehouse",
components: { AddPurchaseDialog },
data() {
return {
addDialogVisible: false,
// 用户导入参数
upload: {
// 是否显示弹出层(用户导入)
@@ -270,7 +275,7 @@ export default {
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 50,
inventory: undefined,
model: undefined,
unit: undefined,

View File

@@ -1,107 +1,154 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<!-- 状态筛选 tabs全部 / 加急 / 未完成 / 已完成 -->
<el-tabs v-model="statusFilter" @tab-click="onStatusTabChange" class="compact-tabs">
<el-tab-pane label="全部" name="all" />
<el-tab-pane name="urgent">
<span slot="label" style="color:#f56c6c;">
<i class="el-icon-warning-outline"></i> 加急 ({{ stat.urgent }})
</span>
</el-tab-pane>
<el-tab-pane :label="`未完成 (${stat.undone})`" name="undone" />
<el-tab-pane :label="`已完成 (${stat.done})`" name="done" />
</el-tabs>
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
label-width="68px" class="compact-search">
<el-form-item label="关联需求" prop="requirementId">
<el-select v-model="queryParams.requirementId" placeholder="选择需求" filterable clearable style="width: 200px">
<el-option v-for="item in requirementList" :key="item.requirementId" :label="item.title"
:value="item.requirementId" />
</el-select>
</el-form-item>
<el-form-item label="操作时间" prop="signTime">
<el-date-picker v-model="searchTime" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']">
</el-date-picker>
:default-time="['00:00:00', '23:59:59']" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete"
v-hasPermi="['oa:oaOutWarehouse:remove']">删除
</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<el-button type="primary" size="mini" icon="el-icon-plus" class="add-purchase-btn"
@click="addDialogVisible = true">新建采购单</el-button>
<add-purchase-dialog :visible.sync="addDialogVisible" @saved="getList" />
<el-table v-loading="loading" :data="TaskList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" />
<el-table-column label="采购单编号" align="center" prop="masterNum">
<template slot-scope="scope">
<el-input v-model="scope.row.masterNum" size="mini" placeholder="请输入采购单编号"
@blur="updateMasterRemark(scope.row)" />
<el-table v-loading="loading" :data="TaskList" @selection-change="handleSelectionChange" stripe size="small"
row-key="masterId" :expand-row-keys="expandedKeys"
:row-class-name="rowClassName" @expand-change="onRowExpand">
<el-table-column type="expand" width="36">
<template slot-scope="props">
<div style="padding: 10px 24px; background:#fafafa;">
<!-- 顶部模式 + 批量入库 -->
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">
<div style="display:flex; align-items:center; gap:8px;">
<span style="font-weight:600;">物料明细{{ (itemsMap[props.row.masterId] || []).length }} </span>
<el-radio-group v-model="mode" size="mini" v-if="props.row.status === 0">
<el-radio-button label="single">单个操作</el-radio-button>
<el-radio-button label="batch">批量操作</el-radio-button>
</el-radio-group>
</div>
<div v-if="mode === 'batch' && props.row.status === 0"
style="display:flex; align-items:center; gap:6px;">
<el-select v-model="batchStatus" size="mini" placeholder="批量设置状态" style="width:140px">
<el-option v-for="s in statusOptions" :key="s.value" :value="s.value" :label="s.label" />
</el-select>
<el-button size="mini" type="success" @click="submitComplete">执行入库</el-button>
</div>
</div>
<el-table v-loading="itemsLoading[props.row.masterId]"
:data="itemsMap[props.row.masterId] || []" size="mini" stripe ref="warehouseTable">
<el-table-column v-if="mode === 'batch' && props.row.status === 0"
type="selection" width="44" align="center" />
<el-table-column label="物料名" prop="name" min-width="120" />
<el-table-column label="截止" prop="endTime" width="120" align="center">
<template slot-scope="s">
<template v-if="s.row.endTime != null && s.row.taskStatus !== 2">
<span v-if="dayDiff(s.row.endTime) > 3">{{ parseTime(s.row.endTime, '{y}-{m}-{d}') }}</span>
<el-tag v-else-if="dayDiff(s.row.endTime) > 0" type="warning" size="mini" effect="plain">{{ dayDiff(s.row.endTime) }}</el-tag>
<el-tag v-else-if="dayDiff(s.row.endTime) === 0" type="danger" size="mini" effect="plain">今日</el-tag>
<el-tag v-else type="danger" size="mini" effect="plain">{{ Math.abs(dayDiff(s.row.endTime)) }}</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="数量" prop="taskInventory" width="64" align="right" />
<el-table-column label="单位" prop="unit" width="56" />
<el-table-column label="单价" width="110" align="right">
<template slot-scope="s">
<el-input v-if="s.row.taskStatus !== 2 && props.row.status === 0"
v-model="s.row.price" size="mini" type="number" placeholder="¥" />
<span v-else>¥{{ Number(s.row.price || 0).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="型号" prop="model" min-width="100" />
<el-table-column label="规格" prop="specifications" min-width="100" />
<el-table-column label="品牌" prop="brand" min-width="80" />
<el-table-column label="备注" prop="remark" min-width="140">
<template slot-scope="s">
<el-input v-model="s.row.remark" size="mini" placeholder="备注"
:disabled="s.row.taskStatus === 2 || props.row.status === 1"
@blur="updateRemark(s.row)" />
</template>
</el-table-column>
<el-table-column label="状态" prop="taskStatus" width="110" align="center">
<template slot-scope="s">
<el-tag v-if="s.row.taskStatus === 2" type="success" size="mini">完成</el-tag>
<el-select v-else-if="mode === 'single' && props.row.status === 0"
v-model="s.row.taskStatus" size="mini" placeholder="状态"
@change="handleUpdateTask(s.row)">
<el-option v-for="opt in filteredStatusOptions(s.row)" :key="opt.value"
:value="opt.value" :label="opt.label" />
</el-select>
<el-tag v-else size="mini">{{ statusLabel(s.row.taskStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="60">
<template slot-scope="s">
<el-button v-if="s.row.taskStatus !== 2 && props.row.status === 0"
size="mini" type="text" style="color:#f56c6c"
@click="handleBatchDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'warning'">{{
scope.row.status === 1 ? "完成" : "未完成"
}}
</el-tag>
</template>
<el-table-column type="selection" width="44" align="center" />
<el-table-column label="操作时间" prop="signTime" width="100">
<template slot-scope="scope">{{ parseTime(scope.row.signTime, "{y}-{m}-{d}") }}</template>
</el-table-column>
<el-table-column label="最近截止日期" align="center" prop="nearestEndTime">
<template slot-scope="scope" v-if="scope.row.nearestEndTime != null && scope.row.status !== 1">
<template v-if="dayDiff(scope.row.nearestEndTime) > 3">
<!-- 超过 3 正常显示 -->
<span>{{ parseTime(scope.row.nearestEndTime, '{y}-{m}-{d}') }}</span>
</template><template v-else-if="dayDiff(scope.row.nearestEndTime) > 0">
<!-- 未来 13 -->
<el-tag type="warning" effect="plain">
剩余{{ dayDiff(scope.row.nearestEndTime) }}
</el-tag>
</template><template v-else-if="dayDiff(scope.row.nearestEndTime) === 0">
<!-- 今天到期 -->
<el-tag type="danger" effect="plain">
<i class="el-icon-warning-outline"></i> 今日过期
</el-tag>
</template><template v-else>
<!-- 已过期 -->
<el-tag type="danger" effect="plain">
逾期{{ Math.abs(dayDiff(scope.row.endTime)) }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="操作时间" align="center" prop="signTime">
<el-table-column label="操作人" prop="signUser" width="90" />
<el-table-column label="关联需求" min-width="220">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.signTime, "{y}-{m}-{d}") }}</span>
</template>
</el-table-column>
<el-table-column label="操作人" align="center" prop="signUser" />
<el-table-column label="备注" align="center" prop="remark">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" size="mini" type="textarea" placeholder="请输入备注"
@blur="updateMasterRemark(scope.row)" />
</template>
</el-table-column>
<el-table-column label="需求编号" align="center" prop="requirementNum">
<template slot-scope="scope">
<el-select v-model="scope.row.requirementId" placeholder="请选择需求编号" filterable clearable
@change="updateMasterRemark(scope.row)">
<el-select v-model="scope.row.requirementId" placeholder="选择需求" filterable clearable size="mini"
style="width: 100%" @change="updateMasterRemark(scope.row)">
<el-option v-for="item in requirementList" :key="item.requirementId" :label="item.title"
:value="item.requirementId" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<el-table-column label="状态" prop="status" width="90" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-search" @click="showDetail(scope.row)">查看
</el-button>
<el-button size="mini" type="text" icon="el-icon-finished" v-if="scope.row.status === 0"
@click="handleIn(scope.row)">执行入库
</el-button>
<el-button size="mini" type="text" icon="el-icon-check" v-if="scope.row.status === 0"
@click="handComplete(scope.row)">完成
</el-button>
<el-button size="mini" type="text" icon="el-icon-download" @click="handleExport(scope.row)">导出
</el-button>
<el-button size="mini" type="text" icon="el-icon-remove" @click="handleDelete(scope.row)">删除
</el-button>
<el-tag :type="scope.row.status === 1 ? 'success' : 'warning'" size="mini">
{{ scope.row.status === 1 ? "已完成" : "未完成" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="200">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" size="mini" placeholder="点击编辑"
@blur="updateMasterRemark(scope.row)" />
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="220" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" v-if="scope.row.status === 0"
@click="expandRow(scope.row)">执行入库</el-button>
<el-button size="mini" type="text" v-if="scope.row.status === 0"
@click="handComplete(scope.row)">完成</el-button>
<el-button size="mini" type="text" @click="handleExport(scope.row)">导出</el-button>
<el-button size="mini" type="text" style="color:#f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -109,126 +156,12 @@
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
<el-drawer size="70%" :title="parseTime(searchItem.signTime) + '-采购单'" :visible.sync="drawer">
<el-table v-loading="loading" :data="warehouseTaskList">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" />
<el-table-column label="物料名" align="center" prop="name" />
<el-table-column label="采购数量" align="center" prop="taskInventory" />
<el-table-column label="单位" align="center" prop="unit" />
<el-table-column label="品牌" align="center" prop="brand" />
<el-table-column label="型号" align="center" prop="model" />
<el-table-column label="规格" align="center" prop="specifications" />
<el-table-column label="操作" align="center" prop="remark">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-edit" @click="handleRemoveTask(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-drawer>
<el-drawer size="70%" :title="parseTime(searchItem.signTime, '{y}_{m}_{d}') + '_采购单'"
:visible.sync="completeDrawer">
<!-- 工具栏 -->
<div style="display:flex;justify-content:space-between;margin-bottom:10px">
<!-- 模式切换 -->
<el-radio-group v-model="mode" size="mini">
<el-radio-button label="single">单个入库</el-radio-button>
<el-radio-button label="batch">批量操作</el-radio-button>
</el-radio-group>
</div>
<el-table v-loading="loading" :data="warehouseTaskList" ref="warehouseTable">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" align="center" type="index" />
<el-table-column label="物料名" align="center" prop="name" />
<el-table-column label="截止日期" align="center" prop="endTime">
<template slot-scope="scope" v-if="scope.row.endTime != null">
<template v-if="dayDiff(scope.row.endTime) > 3">
<!-- 超过 3 正常显示 -->
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
</template><template v-else-if="dayDiff(scope.row.endTime) > 0">
<!-- 未来 13 -->
<el-tag type="warning" effect="plain">
剩余{{ dayDiff(scope.row.endTime) }}
</el-tag>
</template><template v-else-if="dayDiff(scope.row.endTime) === 0">
<!-- 今天到期 -->
<el-tag type="danger" effect="plain">
<i class="el-icon-warning-outline"></i> 今日过期
</el-tag>
</template><template v-else>
<!-- 已过期 -->
<el-tag type="danger" effect="plain">
逾期{{ Math.abs(dayDiff(scope.row.endTime)) }}
</el-tag>
</template>
</template>
</el-table-column>
<el-table-column label="采购数量" align="center" prop="taskInventory" />
<el-table-column label="采购价格" align="center" prop="price">
<template slot-scope="scope">
<el-input v-model="scope.row.price" v-if="scope.row.taskStatus !== 2" type="number"></el-input>
<span v-else>
已经操作入库录入价格请前往入库明细查看
</span>
</template>
</el-table-column>
<el-table-column label="品牌" align="center" prop="brand" />
<el-table-column label="规格" align="center" prop="specifications" />
<el-table-column label="备注" align="center" prop="remark">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" size="mini" placeholder="请输入备注" :disabled="scope.row.taskStatus === 2"
@blur="updateRemark(scope.row)" />
</template>
</el-table-column>
<!-- 操作列单行按钮 -->
<!-- 新增状态列 -->
<el-table-column label="状态" prop="taskStatus" width="140" align="center">
<template slot-scope="scope">
<!-- 单个模式可编辑 -->
<el-tag type="success" v-if="scope.row.taskStatus === 2">完成</el-tag>
<!-- 单个模式下可编辑动态过滤选项 -->
<el-select v-else-if="mode === 'single'" v-model="scope.row.taskStatus" size="mini" placeholder="状态"
@change="handleUpdateTask(scope.row)">
<el-option v-for="s in filteredStatusOptions(scope.row)" :key="s.value" :value="s.value"
:label="s.label" />
</el-select>
<!-- 批量模式只显示 -->
<span v-else>{{ statusLabel(scope.row.taskStatus) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button size="mini" type="danger" v-if="scope.row.taskStatus !== 2"
@click="handleBatchDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="display: flex; justify-content: flex-end; margin: 20px">
<!-- 批量模式下才显示 -->
<div v-if="mode === 'batch'">
<el-select v-model="batchStatus" size="mini" placeholder="选择批量状态">
<el-option v-for="s in statusOptions" :key="s.value" :value="s.value" :label="s.label" />
</el-select>
</div>
<el-button @click="submitComplete" v-if="mode === 'batch'" size="mini" type="success">执行入库</el-button>
<el-button @click="completeDrawer = false" size="mini">关闭</el-button>
</div>
</el-drawer>
</div>
</template>
<script>
import { listRequirements } from "@/api/oa/requirement";
import AddPurchaseDialog from "./components/AddPurchaseDialog.vue";
import {
addOaWarehouseMaster,
delOaWarehouseMaster, listOaWarehouseMaster,
@@ -246,8 +179,19 @@ import {
export default {
name: "OaOutWarehouse",
components: { AddPurchaseDialog },
data () {
return {
// 顶部状态筛选
statusFilter: 'all',
stat: { undone: 0, done: 0, urgent: 0 },
// 物料明细缓存(按 masterId
itemsMap: {},
itemsLoading: {},
// 当前展开的行(单展开)
expandedKeys: [],
// 新建采购单 dialog
addDialogVisible: false,
completeDrawer: false,
mode: 'single',
batchStatus: null, // 批量入库时选择的状态
@@ -484,10 +428,84 @@ export default {
this.queryParams.endTime = '';
}
listOaWarehouseMaster(this.queryParams).then((res) => {
this.TaskList = res.rows;
this.total = res.total;
this.TaskList = res.rows || [];
this.total = res.total || 0;
this.loading = false;
// 重置已展开行的缓存
this.itemsMap = {};
});
this.refreshStat();
},
// 加急判定(备注里含「急」)
isUrgent (remark) {
return !!remark && remark.indexOf('急') !== -1
},
// 加急且未完成 → 行红底
rowClassName ({ row }) {
if (row.status !== 1 && this.isUrgent(row.remark)) return 'row-urgent'
return ''
},
// 拉总览数量(不分页快速 head 计数)
refreshStat () {
const base = { ...this.queryParams, pageNum: 1, pageSize: 1, status: undefined, remark: undefined };
Promise.all([
listOaWarehouseMaster({ ...base, status: 0 }),
listOaWarehouseMaster({ ...base, status: 1 }),
listOaWarehouseMaster({ ...base, remark: '急' })
]).then(([u, d, urg]) => {
this.stat.undone = u.total || 0
this.stat.done = d.total || 0
this.stat.urgent = urg.total || 0
})
},
// tab 切换
onStatusTabChange () {
// 重置过滤
this.queryParams.status = undefined
this.queryParams.remark = undefined
if (this.statusFilter === 'undone') {
this.queryParams.status = 0
} else if (this.statusFilter === 'done') {
this.queryParams.status = 1
} else if (this.statusFilter === 'urgent') {
this.queryParams.remark = '急'
}
this.queryParams.pageNum = 1
this.getList()
},
// 行展开:懒加载物料明细 + 同步 warehouseTaskList兼容老方法
onRowExpand (row, expanded) {
// 单展开:只保留当前
if (expanded && expanded.length) {
this.expandedKeys = [row.masterId]
} else {
this.expandedKeys = this.expandedKeys.filter(k => k !== row.masterId)
}
if (!expanded || !expanded.length) return
const id = row.masterId
this.currentMasterId = id
if (this.itemsMap[id]) {
this.warehouseTaskList = this.itemsMap[id]
return
}
this.$set(this.itemsLoading, id, true)
getOaWarehouseTaskByMasterId(id).then(res => {
const list = res.data || res.rows || []
this.$set(this.itemsMap, id, list)
this.warehouseTaskList = list
}).finally(() => {
this.$set(this.itemsLoading, id, false)
})
},
// 程序化展开(用于"执行入库"按钮)
expandRow (row) {
if (this.expandedKeys.includes(row.masterId)) {
this.expandedKeys = this.expandedKeys.filter(k => k !== row.masterId)
return
}
this.expandedKeys = [row.masterId]
// 触发数据加载
this.onRowExpand(row, [row.masterId])
},
// 取消按钮
cancel () {
@@ -639,3 +657,18 @@ export default {
},
};
</script>
<style lang="scss" scoped>
.add-purchase-btn.el-button {
position: absolute;
top: 12px;
right: 110px; /* 让出 right-toolbar 的位置 */
z-index: 3;
height: 24px;
line-height: 22px;
padding: 0 8px !important;
font-size: 11px !important;
border-radius: 4px;
i { font-size: 11px; margin-right: 2px; }
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form :model="queryParams" ref="queryForm" size="mini" :inline="true" v-show="showSearch"
label-width="68px" class="compact-search">
<el-form-item label="需求标题" prop="title">
<el-input v-model="queryParams.title" placeholder="请输入需求标题" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
@@ -40,41 +41,60 @@
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single"
@click="handleUpdate">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple"
@click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
</el-col>
<el-col :span="2">
<el-button :type="onlyOwnerMe ? 'primary' : 'default'" icon="el-icon-user" size="mini"
@click="toggleOnlyOwnerMe">只看发布给我的</el-button>
</el-col>
<el-col :span="2">
<el-button :type="onlyRequesterMe ? 'primary' : 'default'" icon="el-icon-user-solid" size="mini"
@click="toggleOnlyRequesterMe">只看我发布的</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<div class="req-toolbar">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增</el-button>
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate">修改</el-button>
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport">导出</el-button>
<span class="req-toolbar-sep"></span>
<el-button :type="onlyOwnerMe ? 'primary' : ''" icon="el-icon-user" size="mini" @click="toggleOnlyOwnerMe">只看发布给我的</el-button>
<el-button :type="onlyRequesterMe ? 'primary' : ''" icon="el-icon-user-solid" size="mini" @click="toggleOnlyRequesterMe">只看我发布的</el-button>
</div>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
<!-- 新增提示组件 -->
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange"
@expand-change="onExpandChange">
<el-table-column type="expand" width="36">
<template slot-scope="props">
<div style="padding: 8px 24px; background:#fafafa;">
<div style="font-weight:600; margin-bottom:6px;">
已入库批次{{ (batchMap[props.row.requirementId] || []).length }}
</div>
<el-table v-loading="batchLoading[props.row.requirementId]"
:data="batchMap[props.row.requirementId] || []" size="mini" stripe>
<el-table-column label="入库时间" prop="signTime" width="160">
<template slot-scope="s">{{ parseTime(s.row.signTime, '{y}-{m}-{d} {h}:{i}') }}</template>
</el-table-column>
<el-table-column label="入库单" prop="masterNum" width="160" />
<el-table-column label="入库人" prop="signUser" width="100" />
<el-table-column label="物料概览" prop="summary" min-width="240" show-overflow-tooltip />
<el-table-column label="总数量" prop="totalQty" width="80" align="right" />
<el-table-column label="总金额" width="110" align="right">
<template slot-scope="s">¥{{ Number(s.row.totalAmount || 0).toFixed(2) }}</template>
</el-table-column>
</el-table>
<div v-if="(batchMap[props.row.requirementId] || []).length === 0
&& !batchLoading[props.row.requirementId]"
style="text-align:center; color:#909399; padding:12px 0;">
暂无入库批次
</div>
</div>
</template>
</el-table-column>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="需求标题" align="center" prop="title" />
<el-table-column label="需求方" align="center" prop="requesterNickName" />
<el-table-column label="负责人" align="center" prop="ownerNickName" />
<el-table-column label="关联项目" align="center" prop="projectName" />
<el-table-column label="需求描述" align="center" prop="description" />
<el-table-column label="需求标题" align="center" prop="title" min-width="160" show-overflow-tooltip />
<el-table-column label="需求方" align="center" prop="requesterNickName" width="100" show-overflow-tooltip />
<el-table-column label="负责人" align="center" prop="ownerNickName" width="100" show-overflow-tooltip />
<el-table-column label="关联项目" align="center" prop="projectName" min-width="160" show-overflow-tooltip />
<el-table-column label="需求描述" align="center" prop="description" min-width="200" show-overflow-tooltip>
<template slot-scope="{ row }">
<span v-if="row.description" class="copyable-text" @click="copyText(row.description)"
title="点击复制">{{ row.description }}</span>
<span v-else style="color:#c0c4cc;">无</span>
</template>
</el-table-column>
<el-table-column label="开始时间" align="center" prop="createTime" width="180">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
@@ -105,9 +125,18 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="附件" align="center" prop="accessory" max-width="180">
<el-table-column label="附件" align="center" prop="accessoryFiles" width="180">
<template slot-scope="{ row }">
<file-preview v-model="row.accessory" :simple="true" />
<template v-if="parseAccessoryFiles(row.accessoryFiles).length">
<el-tooltip v-for="f in parseAccessoryFiles(row.accessoryFiles)" :key="f.ossId"
:content="f.name" placement="top" effect="dark">
<a class="accessory-link" :href="f.url || 'javascript:void(0)'"
target="_blank" @click="!f.url && downloadOss(f.ossId)">
<i class="el-icon-paperclip"></i>{{ f.name }}
</a>
</el-tooltip>
</template>
<span v-else style="color:#c0c4cc;">无</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
@@ -189,7 +218,7 @@
</template>
<script>
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@/api/oa/requirement";
import { addRequirements, delRequirements, getRequirements, getRequirementBatches, listRequirements, updateRequirements } from "@/api/oa/requirement";
import { listUser } from "@/api/system/user";
import FilePreview from '@/components/FilePreview';
import FileUpload from '@/components/FileUpload';
@@ -200,6 +229,9 @@ export default {
components: { FileUpload, FilePreview, ProjectSelect },
data () {
return {
// 入库批次(按 requirementId 缓存)
batchMap: {},
batchLoading: {},
// 按钮loading
buttonLoading: false,
// 遮罩层
@@ -225,7 +257,7 @@ export default {
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
pageSize: 50,
title: undefined,
requesterId: undefined,
ownerId: undefined,
@@ -270,6 +302,55 @@ export default {
this.getUsers();
},
methods: {
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
parseAccessoryFiles (raw) {
if (!raw) return []
return String(raw).split(',,').map(s => {
const [ossId, name, url] = s.split('|')
return { ossId, name: name || '附件', url: url || '' }
}).filter(f => f.ossId)
},
copyText (text) {
if (!text) return
const fallback = () => {
const ta = document.createElement('textarea')
ta.value = text
ta.style.position = 'fixed'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
try { document.execCommand('copy'); this.$modal.msgSuccess('复制成功') }
catch (e) { this.$modal.msgError('复制失败,请手动选中') }
document.body.removeChild(ta)
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text)
.then(() => this.$modal.msgSuccess('复制成功'))
.catch(fallback)
} else {
fallback()
}
},
downloadOss (ossId) {
if (this.$download && this.$download.oss) {
this.$download.oss(ossId)
} else {
const file = this.ossFileMap[ossId]
if (file && file.url) window.open(file.url, '_blank')
}
},
// 展开行:加载该需求的入库批次
onExpandChange (row, expanded) {
if (!expanded || !expanded.length) return
const id = row.requirementId
if (this.batchMap[id]) return // 已有缓存
this.$set(this.batchLoading, id, true)
getRequirementBatches(id).then(res => {
this.$set(this.batchMap, id, res.data || [])
}).finally(() => {
this.$set(this.batchLoading, id, false)
})
},
async onStatusChange (row, newVal) {
row._updating = true
// 如果后端需要字符串,可改为 String(newVal)
@@ -485,3 +566,43 @@ export default {
}
};
</script>
<style lang="scss" scoped>
.req-toolbar {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
margin-bottom: 6px;
::v-deep .el-button--mini {
padding: 4px 10px !important;
font-size: 11px !important;
height: 24px;
}
}
.req-toolbar-sep {
display: inline-block;
width: 1px;
height: 16px;
background: #e4e7ed;
margin: 0 4px;
}
.copyable-text {
cursor: pointer;
&:hover { color: #409eff; text-decoration: underline; }
}
.accessory-link {
display: inline-block;
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
color: #409eff;
font-size: 11px;
padding: 0 4px;
i { margin-right: 2px; }
&:hover { text-decoration: underline; }
& + .accessory-link { border-left: 1px solid #ebeef5; }
}
</style>