采购需求添加绑定物料

This commit is contained in:
2026-06-13 15:37:10 +08:00
parent 2575483122
commit b7af1b87ab
7 changed files with 430 additions and 56 deletions

View File

@@ -45,6 +45,10 @@ public class OaRequirements extends BaseEntity {
* 挂接项目 ID可选
*/
private Long projectId;
/**
* 关联物料 ID CSV -> sys_oa_warehouse.id可选
*/
private String materialIds;
/**
* 需求描述
*/

View File

@@ -49,6 +49,11 @@ public class OaRequirementsBo extends BaseEntity {
*/
private Long projectId;
/**
* 关联物料 ID CSV多个物料用逗号分隔
*/
private String materialIds;
/**
* 需求描述
*/

View File

@@ -89,6 +89,22 @@ public class OaRequirementsVo extends BaseEntity {
private String ownerNickName;
private String projectName;
/** 关联物料 ID CSV */
private String materialIds;
/** 关联物料明细service 层 enrich列表/详情都返回) */
private java.util.List<MaterialItem> materials;
@Data
public static class MaterialItem implements java.io.Serializable {
private Long id;
private String name;
private String model;
private String unit;
private Long inventory;
private String brand;
private String specifications;
}
/** 附件文件列表(已联查 sys_oss每项形如 "<ossId>|<originalName>|<url>",逗号分隔) */
private String accessoryFiles;

View File

@@ -15,13 +15,19 @@ import org.springframework.stereotype.Service;
import com.ruoyi.oa.domain.bo.OaRequirementsBo;
import com.ruoyi.oa.domain.vo.OaRequirementsVo;
import com.ruoyi.oa.domain.OaRequirements;
import com.ruoyi.oa.domain.SysOaWarehouse;
import com.ruoyi.oa.mapper.OaRequirementsMapper;
import com.ruoyi.oa.mapper.SysOaWarehouseMapper;
import com.ruoyi.oa.service.IOaRequirementsService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Collection;
import java.util.stream.Collectors;
/**
* OA 需求Service业务层处理
@@ -37,12 +43,63 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
private final OaWarehouseAuditService auditService;
private final SysOaWarehouseMapper warehouseMapper;
/**
* 查询OA 需求
*/
@Override
public OaRequirementsVo queryById(Long requirementId){
return baseMapper.selectVoById(requirementId);
OaRequirementsVo vo = baseMapper.selectVoById(requirementId);
enrichMaterials(Collections.singletonList(vo));
return vo;
}
/** 根据 material_idsCSV批量补全物料明细到 VO.materials */
private void enrichMaterials(List<OaRequirementsVo> list) {
if (list == null || list.isEmpty()) return;
Set<Long> ids = new HashSet<>();
for (OaRequirementsVo v : list) {
if (v == null) continue;
for (Long id : parseCsv(v.getMaterialIds())) ids.add(id);
}
if (ids.isEmpty()) return;
List<SysOaWarehouse> wList = warehouseMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SysOaWarehouse>()
.in("id", ids));
Map<Long, SysOaWarehouse> wMap = wList.stream()
.collect(Collectors.toMap(SysOaWarehouse::getId, w -> w, (a, b) -> a));
for (OaRequirementsVo v : list) {
if (v == null) continue;
List<Long> mids = parseCsv(v.getMaterialIds());
if (mids.isEmpty()) continue;
List<OaRequirementsVo.MaterialItem> items = new ArrayList<>();
for (Long id : mids) {
SysOaWarehouse w = wMap.get(id);
if (w == null) continue;
OaRequirementsVo.MaterialItem it = new OaRequirementsVo.MaterialItem();
it.setId(w.getId());
it.setName(w.getName());
it.setModel(w.getModel());
it.setUnit(w.getUnit());
it.setInventory(w.getInventory());
it.setBrand(w.getBrand());
it.setSpecifications(w.getSpecifications());
items.add(it);
}
v.setMaterials(items);
}
}
private List<Long> parseCsv(String csv) {
if (csv == null || csv.isEmpty()) return Collections.emptyList();
List<Long> r = new ArrayList<>();
for (String s : csv.split(",")) {
String t = s.trim();
if (t.isEmpty()) continue;
try { r.add(Long.parseLong(t)); } catch (NumberFormatException ignored) {}
}
return r;
}
/**
@@ -52,6 +109,7 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
public TableDataInfo<OaRequirementsVo> queryPageList(OaRequirementsBo bo, PageQuery pageQuery) {
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
Page<OaRequirementsVo> result = baseMapper.selectVoListPage(pageQuery.build(), lqw);
enrichMaterials(result.getRecords());
return TableDataInfo.build(result);
}
@@ -61,7 +119,9 @@ public class OaRequirementsServiceImpl implements IOaRequirementsService {
@Override
public List<OaRequirementsVo> queryList(OaRequirementsBo bo) {
QueryWrapper<OaRequirements> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoListForExport(lqw);
List<OaRequirementsVo> list = baseMapper.selectVoListForExport(lqw);
enrichMaterials(list);
return list;
}
private QueryWrapper<OaRequirements> buildQueryWrapper(OaRequirementsBo bo) {

View File

@@ -68,6 +68,8 @@ public class SysOaWarehouseServiceImpl implements ISysOaWarehouseService {
.like("sow.brand", bo.getName())
.or()
.like("sow.model", bo.getName())
.or()
.like("sow.specifications", bo.getName())
)
.eq(StringUtils.isNotBlank(bo.getModel()), "sow.model", bo.getModel())
.eq(StringUtils.isNotBlank(bo.getBrand()), "sow.brand", bo.getBrand());

View File

@@ -66,40 +66,25 @@
<!-- 新增提示组件 -->
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
<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 v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<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="left" min-width="240">
<template slot-scope="{ row }">
<template v-if="row.materials && row.materials.length">
<div v-for="m in row.materials" :key="m.id" class="mat-row">
<span class="mat-name">{{ m.name }}<span v-if="m.model" class="mat-model"> / {{ m.model }}</span></span>
<span class="mat-stock" :class="{ low: (m.inventory||0) <= 0 }">
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
</span>
</div>
</template>
<span v-else style="color:#c0c4cc;">未关联</span>
</template>
</el-table-column>
<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)"
@@ -153,9 +138,6 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-truck" style="color:#409eff"
v-if="scope.row.status !== 2 && scope.row.status !== 3"
@click="handleGoToInbound(scope.row)">执行入库</el-button>
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
v-if="scope.row.status === 1">完成</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
@@ -184,6 +166,25 @@
<el-form-item label="关联项目" prop="projectId">
<project-select v-model="form.projectId" style="width: 100%" />
</el-form-item>
<el-form-item label="采购物料" prop="materialIdArr">
<div class="mat-trigger">
<el-button size="mini" icon="el-icon-search" @click="openMaterialPicker">
选择物料{{ selectedMaterials.length ? `(已选 ${selectedMaterials.length}` : '' }}
</el-button>
<span v-if="!selectedMaterials.length" class="mat-trigger-hint">可多选;留空也可保存,后续再关联</span>
</div>
<div v-if="selectedMaterials.length" class="mat-stock-panel">
<div v-for="m in selectedMaterials" :key="m.id" class="mat-stock-row">
<span class="mat-stock-name">{{ m.name }}<span v-if="m.model" style="color:#909399;"> / {{ m.model }}</span></span>
<span class="mat-stock-tag" :class="{ low: (m.inventory||0) <= 0 }">
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
</span>
<span v-if="m.brand" class="mat-stock-meta">品牌:{{ m.brand }}</span>
<span v-if="m.specifications" class="mat-stock-meta">规格:{{ m.specifications }}</span>
<i class="el-icon-close mat-remove" title="移除" @click="removeSelectedMaterial(m.id)"></i>
</div>
</div>
</el-form-item>
<el-form-item label="需求描述" prop="description">
<el-input v-model="form.description" type="textarea" placeholder="请输入需求描述" />
</el-form-item>
@@ -205,6 +206,84 @@
</div>
</el-dialog>
<!-- 物料选择器 -->
<el-dialog title="选择采购物料" :visible.sync="materialPickerOpen" width="780px"
append-to-body :close-on-click-modal="false" @open="onPickerOpen">
<div class="picker-toolbar">
<el-input v-model="picker.kw" placeholder="搜索 名称 / 型号 / 品牌 / 规格"
size="mini" clearable prefix-icon="el-icon-search"
style="width: 320px;" @input="onPickerSearch" @clear="onPickerSearch" />
<el-button size="mini" type="primary" plain icon="el-icon-plus"
style="margin-left:8px;" @click="openInlineNew">未找到?新增物料</el-button>
<span class="picker-tip" v-if="picker.tempSelected.length">
已勾选 <b>{{ picker.tempSelected.length }}</b> 项
</span>
</div>
<!-- 内嵌新增物料表单 -->
<el-card v-if="newMat.show" shadow="never" class="new-mat-card">
<div slot="header" class="new-mat-header">
<span><i class="el-icon-plus" /> 新增物料到库存</span>
<el-button type="text" size="mini" icon="el-icon-close" @click="newMat.show = false">收起</el-button>
</div>
<el-form :model="newMat" :rules="newMatRules" ref="newMatForm" size="mini"
label-width="70px" :inline="true">
<el-form-item label="名称" prop="name" required>
<el-input v-model="newMat.name" placeholder="物料名称" style="width:180px;" />
</el-form-item>
<el-form-item label="型号" prop="model">
<el-input v-model="newMat.model" placeholder="可选" style="width:140px;" />
</el-form-item>
<el-form-item label="规格" prop="specifications">
<el-input v-model="newMat.specifications" placeholder="可选" style="width:140px;" />
</el-form-item>
<el-form-item label="品牌" prop="brand">
<el-input v-model="newMat.brand" placeholder="可选" style="width:120px;" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="newMat.unit" placeholder="//kg" style="width:100px;" />
</el-form-item>
<el-form-item label="初始库存" prop="inventory" required>
<el-input-number v-model="newMat.inventory" :min="0" :step="1" size="mini" />
</el-form-item>
<el-form-item>
<el-button size="mini" type="primary" :loading="newMat.saving" @click="submitNewMat">保存并选中</el-button>
<el-button size="mini" @click="newMat.show = false">取消</el-button>
</el-form-item>
</el-form>
</el-card>
<el-table :data="picker.list" v-loading="picker.loading" size="mini"
ref="pickerTable" border highlight-current-row max-height="380"
row-key="id" :row-class-name="pickerRowClass"
@row-click="onPickerRowClick"
@selection-change="onPickerSelectionChange">
<el-table-column type="selection" width="42" reserve-selection />
<el-table-column label="名称" prop="name" min-width="140" show-overflow-tooltip />
<el-table-column label="型号" prop="model" width="120" show-overflow-tooltip />
<el-table-column label="规格" prop="specifications" width="110" show-overflow-tooltip />
<el-table-column label="品牌" prop="brand" width="100" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="60" align="center" />
<el-table-column label="库存" width="90" align="right">
<template slot-scope="s">
<span :style="{color: (s.row.inventory||0) <= 0 ? '#f56c6c' : '#67c23a', fontWeight:600}">
{{ s.row.inventory == null ? 0 : s.row.inventory }}
</span>
</template>
</el-table-column>
</el-table>
<div class="picker-pager" v-if="picker.total > picker.pageSize">
<el-pagination small background layout="prev, pager, next, total"
:total="picker.total" :page-size="picker.pageSize" :current-page.sync="picker.pageNum"
@current-change="loadPickerList" />
</div>
<div slot="footer">
<el-button @click="materialPickerOpen = false">取 消</el-button>
<el-button type="primary" @click="confirmPicker">确定({{ picker.tempSelected.length }}</el-button>
</div>
</el-dialog>
<!-- 需求详情对话框 -->
<el-dialog title="需求详情" :visible.sync="detailDialog" width="600px" append-to-body>
<el-descriptions :column="1" border>
@@ -233,8 +312,9 @@
</template>
<script>
import { addRequirements, delRequirements, getRequirements, getRequirementBatches, listRequirements, updateRequirements } from "@/api/oa/requirement";
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@/api/oa/requirement";
import { listUser } from "@/api/system/user";
import { listOaWarehouse, getOaWarehouse, addOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
import FilePreview from '@/components/FilePreview';
import FileUpload from '@/components/FileUpload';
import ProjectSelect from "@/components/fad-service/ProjectSelect";
@@ -244,9 +324,35 @@ export default {
components: { FileUpload, FilePreview, ProjectSelect },
data () {
return {
// 入库批次(按 requirementId 缓存
batchMap: {},
batchLoading: {},
// 当前已选物料明细(用于库存展示
selectedMaterials: [],
// 物料选择器
materialPickerOpen: false,
picker: {
kw: '',
loading: false,
list: [],
total: 0,
pageNum: 1,
pageSize: 10,
tempSelected: [], // 选择器内部勾选项(含完整物料对象)
searchTimer: null,
},
// 新增物料内嵌表单
newMat: {
show: false,
saving: false,
name: '',
model: '',
specifications: '',
brand: '',
unit: '',
inventory: 0,
},
newMatRules: {
name: [{ required: true, message: '物料名称不能为空', trigger: 'blur' }],
inventory: [{ required: true, message: '初始库存不能为空', trigger: 'change' }],
},
// 按钮loading
buttonLoading: false,
// 遮罩层
@@ -324,11 +430,120 @@ export default {
this.refreshStat();
},
methods: {
// 跳到入库明细页面,并预填该采购需求
handleGoToInbound (row) {
this.$router.push({
path: '/step/in',
query: { requirementId: String(row.requirementId), requirementTitle: row.title }
// ====== 物料选择器 ======
openMaterialPicker () {
this.picker.kw = ''
this.picker.pageNum = 1
// 当前已选项作为初始勾选
this.picker.tempSelected = this.selectedMaterials.slice()
this.newMat.show = false
this.materialPickerOpen = true
},
onPickerOpen () {
this.loadPickerList().then(() => this.syncPickerSelectionUI())
},
loadPickerList () {
this.picker.loading = true
const params = {
pageNum: this.picker.pageNum,
pageSize: this.picker.pageSize,
}
if (this.picker.kw && this.picker.kw.trim()) params.name = this.picker.kw.trim()
return listOaWarehouse(params).then(res => {
this.picker.list = res.rows || []
this.picker.total = res.total || 0
this.$nextTick(() => this.syncPickerSelectionUI())
}).finally(() => { this.picker.loading = false })
},
onPickerSearch () {
clearTimeout(this.picker.searchTimer)
this.picker.searchTimer = setTimeout(() => {
this.picker.pageNum = 1
this.loadPickerList()
}, 250)
},
// 表格分页/刷新后把已勾选项重新打勾
syncPickerSelectionUI () {
const tbl = this.$refs.pickerTable
if (!tbl) return
tbl.clearSelection()
const ids = new Set(this.picker.tempSelected.map(m => m.id))
for (const row of this.picker.list) {
if (ids.has(row.id)) tbl.toggleRowSelection(row, true)
}
},
onPickerSelectionChange (rows) {
// 当前页勾选 + 之前其它页保留下来的(不在 list 中的)
const curIds = new Set(this.picker.list.map(m => m.id))
const keepOther = this.picker.tempSelected.filter(m => !curIds.has(m.id))
this.picker.tempSelected = keepOther.concat(rows)
},
onPickerRowClick (row) {
const tbl = this.$refs.pickerTable
if (!tbl) return
const idx = this.picker.tempSelected.findIndex(m => m.id === row.id)
tbl.toggleRowSelection(row, idx === -1)
},
pickerRowClass ({ row }) {
return this.picker.tempSelected.find(m => m.id === row.id) ? 'picker-row-checked' : ''
},
confirmPicker () {
this.selectedMaterials = this.picker.tempSelected.slice()
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
this.materialPickerOpen = false
},
removeSelectedMaterial (id) {
this.selectedMaterials = this.selectedMaterials.filter(m => m.id !== id)
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
},
// ====== 新增物料 ======
openInlineNew () {
this.newMat.show = true
this.newMat.name = this.picker.kw || ''
this.newMat.model = ''
this.newMat.specifications = ''
this.newMat.brand = ''
this.newMat.unit = ''
this.newMat.inventory = 0
this.$nextTick(() => { this.$refs.newMatForm && this.$refs.newMatForm.clearValidate() })
},
submitNewMat () {
this.$refs.newMatForm.validate(valid => {
if (!valid) return
this.newMat.saving = true
const payload = {
name: this.newMat.name.trim(),
model: this.newMat.model || undefined,
specifications: this.newMat.specifications || undefined,
brand: this.newMat.brand || undefined,
unit: this.newMat.unit || undefined,
inventory: Number(this.newMat.inventory) || 0,
}
addOaWarehouse(payload).then(res => {
// 后端返回的 data 可能是 id 或对象,统一兜底再 get 一次
const newId = (res && (res.data && (res.data.id || typeof res.data === 'number'))) ? (res.data.id || res.data) : null
const finish = (full) => {
if (!full) return
// 直接放进当前页顶端 + 勾选
if (!this.picker.list.find(m => m.id === full.id)) {
this.picker.list.unshift(full)
}
if (!this.picker.tempSelected.find(m => m.id === full.id)) {
this.picker.tempSelected.push(full)
}
this.syncPickerSelectionUI()
this.newMat.show = false
this.$modal.msgSuccess('已新增并自动选中')
}
if (newId) {
getOaWarehouse(newId).then(r => finish(r.data))
} else {
// 兜底:用刚提交的字段拼一个临时对象
finish({ ...payload, id: Date.now() })
// 重新查一下保证 id 真实
this.loadPickerList()
}
}).finally(() => { this.newMat.saving = false })
})
},
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
@@ -368,18 +583,6 @@ export default {
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)
@@ -453,6 +656,8 @@ export default {
requesterId: undefined,
ownerId: undefined,
projectId: undefined,
materialIds: undefined,
materialIdArr: [],
description: undefined,
deadline: undefined,
status: 0,
@@ -488,6 +693,7 @@ export default {
handleAdd () {
this.reset();
this.form.requesterId = this.$store.state.user.id;
this.selectedMaterials = [];
this.open = true;
this.title = "添加OA 需求";
},
@@ -504,10 +710,21 @@ export default {
handleUpdate (row) {
this.loading = true;
this.reset();
this.selectedMaterials = [];
const requirementId = row.requirementId || this.ids
getRequirements(requirementId).then(response => {
this.loading = false;
this.form = response.data;
const data = response.data || {};
const ids = (data.materialIds || '').split(',').map(s => Number(s)).filter(n => !isNaN(n) && n)
data.materialIdArr = ids
this.form = data;
if (data.materials && data.materials.length) {
this.selectedMaterials = data.materials.slice()
} else if (ids.length) {
// 兜底:逐个拉
Promise.all(ids.map(id => getOaWarehouse(id).then(r => r.data).catch(() => null)))
.then(list => { this.selectedMaterials = list.filter(Boolean) })
}
this.open = true;
this.title = "修改OA 需求";
});
@@ -517,6 +734,8 @@ export default {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
const arr = Array.isArray(this.form.materialIdArr) ? this.form.materialIdArr : []
this.form.materialIds = arr.length ? arr.join(',') : null
if (this.form.requirementId != null) {
updateRequirements(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
@@ -652,6 +871,67 @@ export default {
cursor: pointer;
&:hover { color: #409eff; text-decoration: underline; }
}
.mat-trigger {
display: flex; align-items: center; gap: 8px;
.mat-trigger-hint { font-size: 12px; color: #909399; }
}
.mat-remove {
margin-left: auto;
cursor: pointer;
color: #c0c4cc;
&:hover { color: #f56c6c; }
}
.picker-toolbar {
display: flex; align-items: center; margin-bottom: 10px;
.picker-tip { margin-left: auto; font-size: 12px; color: #606266; }
}
.picker-pager { text-align: right; margin-top: 10px; }
.new-mat-card {
margin-bottom: 10px;
::v-deep .el-card__header { padding: 6px 12px; background: #f5f7fa; }
.new-mat-header {
display: flex; align-items: center; justify-content: space-between;
font-size: 13px; color: #303133;
}
}
::v-deep .picker-row-checked > td { background: #ecf5ff !important; }
.mat-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
line-height: 1.6;
& + .mat-row { border-top: 1px dashed #ebeef5; }
.mat-name { flex: 1; color: #303133; }
.mat-model { color: #909399; margin-left: 2px; }
.mat-stock {
flex-shrink: 0;
padding: 0 6px;
border-radius: 3px;
background: #f0f9eb; color: #67c23a;
&.low { background: #fef0f0; color: #f56c6c; }
}
}
.mat-stock-panel {
margin-top: 4px;
padding: 6px 8px;
background: #fafafa;
border-radius: 3px;
.mat-stock-row {
display: flex; flex-wrap: wrap; gap: 6px;
align-items: center;
font-size: 12px;
line-height: 1.8;
& + .mat-stock-row { border-top: 1px dashed #ebeef5; }
}
.mat-stock-name { flex: 1; min-width: 120px; color: #303133; }
.mat-stock-tag {
padding: 0 6px; border-radius: 3px;
background: #f0f9eb; color: #67c23a;
&.low { background: #fef0f0; color: #f56c6c; }
}
.mat-stock-meta { color: #909399; }
}
.accessory-link {
display: inline-block;
max-width: 160px;

View File

@@ -0,0 +1,7 @@
-- 采购需求关联物料库存支持多物料CSV
ALTER TABLE oa_requirements
ADD COLUMN material_ids VARCHAR(1000) DEFAULT NULL COMMENT '关联物料ID CSV -> sys_oa_warehouse.id' AFTER project_id;
-- 已应用过单物料版本material_id的环境执行下面这句把旧字段迁移并删除
-- UPDATE oa_requirements SET material_ids = CAST(material_id AS CHAR) WHERE material_id IS NOT NULL AND (material_ids IS NULL OR material_ids = '');
-- ALTER TABLE oa_requirements DROP COLUMN material_id;