feat(wms): 添加智能库存助手功能

- 集成 OpenAI 模型配置,支持自然语言库存操作
- 在移动端界面添加智能助手交互组件
- 实现库存操作计划生成与执行流程
- 添加入库登记的修改和删除功能
- 新增 agent 相关 API 接口和数据模型
- 支持上下文关联和连续操作处理
- 提供操作预览和确认机制
- 完善前后端交互和状态管理逻辑
This commit is contained in:
lixiaolong 2026-07-16 14:16:55 +08:00
parent 79bbf60817
commit 7737fda061
12 changed files with 2679 additions and 11 deletions

View File

@ -0,0 +1,32 @@
package com.ruoyi.web.controller.warehouse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.warehouse.domain.agent.AgentAnalyzeRequest;
import com.ruoyi.warehouse.domain.agent.AgentExecuteRequest;
import com.ruoyi.warehouse.service.IAgentService;
@RestController
@RequestMapping("/warehouse/agent")
public class AgentController extends BaseController
{
@Autowired
private IAgentService agentService;
@PostMapping("/analyze")
public AjaxResult analyze(@RequestBody AgentAnalyzeRequest request)
{
return success(agentService.analyze(request.getText(), request.getContextResults(), getUsername()));
}
@PostMapping("/execute")
public AjaxResult execute(@RequestBody AgentExecuteRequest request)
{
return success(agentService.execute(request.getPlan(), getUsername()));
}
}

View File

@ -142,3 +142,10 @@ xss:
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# WMS agent model config. Keep api-key in environment variables, not in source code.
agent:
openai:
base-url: ${OPENAI_BASE_URL:http://192.168.28.10:18080}
api-key: ${OPENAI_API_KEY:sk-dd185946191d42eaa54892dec488bf7b54cad2d7433aee8aac7a017365b0829d}
model: ${OPENAI_MODEL:gpt-5.5}

View File

@ -0,0 +1,30 @@
package com.ruoyi.warehouse.domain.agent;
import java.util.ArrayList;
import java.util.List;
public class AgentAnalyzeRequest
{
private String text;
private List<AgentResultItem> contextResults = new ArrayList<>();
public String getText()
{
return text;
}
public void setText(String text)
{
this.text = text;
}
public List<AgentResultItem> getContextResults()
{
return contextResults;
}
public void setContextResults(List<AgentResultItem> contextResults)
{
this.contextResults = contextResults;
}
}

View File

@ -0,0 +1,61 @@
package com.ruoyi.warehouse.domain.agent;
public class AgentCandidate
{
private Long id;
private String type;
private String name;
private String detail;
public AgentCandidate()
{
}
public AgentCandidate(Long id, String type, String name, String detail)
{
this.id = id;
this.type = type;
this.name = name;
this.detail = detail;
}
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getType()
{
return type;
}
public void setType(String type)
{
this.type = type;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getDetail()
{
return detail;
}
public void setDetail(String detail)
{
this.detail = detail;
}
}

View File

@ -0,0 +1,16 @@
package com.ruoyi.warehouse.domain.agent;
public class AgentExecuteRequest
{
private AgentPlan plan;
public AgentPlan getPlan()
{
return plan;
}
public void setPlan(AgentPlan plan)
{
this.plan = plan;
}
}

View File

@ -0,0 +1,317 @@
package com.ruoyi.warehouse.domain.agent;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class AgentPlan
{
private String action;
private String summary;
private String message;
private boolean executable;
private boolean needConfirm;
private Long areaId;
private Long productId;
private Long stockId;
private Long recordId;
private Integer referenceIndex;
private String areaName;
private String productName;
private String brand;
private String category;
private String spec;
private String unit;
private BigDecimal quantity;
private Integer inType;
private Integer outType;
private String operatorName;
private String recordNo;
private String remark;
private BigDecimal currentStock;
private BigDecimal afterStock;
private List<String> warnings = new ArrayList<>();
private List<AgentCandidate> candidates = new ArrayList<>();
private List<AgentResultItem> results = new ArrayList<>();
private AgentPlan nextPlan;
public String getAction()
{
return action;
}
public void setAction(String action)
{
this.action = action;
}
public String getSummary()
{
return summary;
}
public void setSummary(String summary)
{
this.summary = summary;
}
public String getMessage()
{
return message;
}
public void setMessage(String message)
{
this.message = message;
}
public boolean isExecutable()
{
return executable;
}
public void setExecutable(boolean executable)
{
this.executable = executable;
}
public boolean isNeedConfirm()
{
return needConfirm;
}
public void setNeedConfirm(boolean needConfirm)
{
this.needConfirm = needConfirm;
}
public Long getAreaId()
{
return areaId;
}
public void setAreaId(Long areaId)
{
this.areaId = areaId;
}
public Long getProductId()
{
return productId;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public Long getStockId()
{
return stockId;
}
public void setStockId(Long stockId)
{
this.stockId = stockId;
}
public Long getRecordId()
{
return recordId;
}
public void setRecordId(Long recordId)
{
this.recordId = recordId;
}
public Integer getReferenceIndex()
{
return referenceIndex;
}
public void setReferenceIndex(Integer referenceIndex)
{
this.referenceIndex = referenceIndex;
}
public String getAreaName()
{
return areaName;
}
public void setAreaName(String areaName)
{
this.areaName = areaName;
}
public String getProductName()
{
return productName;
}
public void setProductName(String productName)
{
this.productName = productName;
}
public String getBrand()
{
return brand;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public String getCategory()
{
return category;
}
public void setCategory(String category)
{
this.category = category;
}
public String getSpec()
{
return spec;
}
public void setSpec(String spec)
{
this.spec = spec;
}
public String getUnit()
{
return unit;
}
public void setUnit(String unit)
{
this.unit = unit;
}
public BigDecimal getQuantity()
{
return quantity;
}
public void setQuantity(BigDecimal quantity)
{
this.quantity = quantity;
}
public Integer getInType()
{
return inType;
}
public void setInType(Integer inType)
{
this.inType = inType;
}
public Integer getOutType()
{
return outType;
}
public void setOutType(Integer outType)
{
this.outType = outType;
}
public String getOperatorName()
{
return operatorName;
}
public void setOperatorName(String operatorName)
{
this.operatorName = operatorName;
}
public String getRecordNo()
{
return recordNo;
}
public void setRecordNo(String recordNo)
{
this.recordNo = recordNo;
}
public String getRemark()
{
return remark;
}
public void setRemark(String remark)
{
this.remark = remark;
}
public BigDecimal getCurrentStock()
{
return currentStock;
}
public void setCurrentStock(BigDecimal currentStock)
{
this.currentStock = currentStock;
}
public BigDecimal getAfterStock()
{
return afterStock;
}
public void setAfterStock(BigDecimal afterStock)
{
this.afterStock = afterStock;
}
public List<String> getWarnings()
{
return warnings;
}
public void setWarnings(List<String> warnings)
{
this.warnings = warnings;
}
public List<AgentCandidate> getCandidates()
{
return candidates;
}
public void setCandidates(List<AgentCandidate> candidates)
{
this.candidates = candidates;
}
public List<AgentResultItem> getResults()
{
return results;
}
public void setResults(List<AgentResultItem> results)
{
this.results = results;
}
public AgentPlan getNextPlan()
{
return nextPlan;
}
public void setNextPlan(AgentPlan nextPlan)
{
this.nextPlan = nextPlan;
}
}

View File

@ -0,0 +1,128 @@
package com.ruoyi.warehouse.domain.agent;
import java.math.BigDecimal;
public class AgentResultItem
{
private Long id;
private Long areaId;
private Long productId;
private String areaName;
private String productName;
private String brand;
private String category;
private String spec;
private String unit;
private BigDecimal stockNum;
private String recordNo;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public Long getAreaId()
{
return areaId;
}
public void setAreaId(Long areaId)
{
this.areaId = areaId;
}
public Long getProductId()
{
return productId;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public String getAreaName()
{
return areaName;
}
public void setAreaName(String areaName)
{
this.areaName = areaName;
}
public String getProductName()
{
return productName;
}
public void setProductName(String productName)
{
this.productName = productName;
}
public String getBrand()
{
return brand;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public String getCategory()
{
return category;
}
public void setCategory(String category)
{
this.category = category;
}
public String getSpec()
{
return spec;
}
public void setSpec(String spec)
{
this.spec = spec;
}
public String getUnit()
{
return unit;
}
public void setUnit(String unit)
{
this.unit = unit;
}
public BigDecimal getStockNum()
{
return stockNum;
}
public void setStockNum(BigDecimal stockNum)
{
this.stockNum = stockNum;
}
public String getRecordNo()
{
return recordNo;
}
public void setRecordNo(String recordNo)
{
this.recordNo = recordNo;
}
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.warehouse.service;
import java.util.List;
import com.ruoyi.warehouse.domain.agent.AgentPlan;
import com.ruoyi.warehouse.domain.agent.AgentResultItem;
public interface IAgentService
{
public AgentPlan analyze(String text, List<AgentResultItem> contextResults, String username);
public AgentPlan execute(AgentPlan plan, String username);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,21 @@
import request from '@/utils/request'
export function analyzeAgent(data) {
return request({
url: '/warehouse/agent/analyze',
method: 'post',
data,
headers: {
repeatSubmit: false
},
timeout: 45000
})
}
export function executeAgent(data) {
return request({
url: '/warehouse/agent/execute',
method: 'post',
data
})
}

View File

@ -8,6 +8,68 @@
<el-button type="primary" icon="el-icon-mobile-phone" @click="$router.push('/mobile')">移动端入口</el-button>
</section>
<section class="agent-panel">
<div class="agent-title">
<div>
<h2>智能库存助手</h2>
<p>输入一句话系统先生成操作草稿确认后再真正执行</p>
</div>
<el-tag size="small" type="info">第一阶段</el-tag>
</div>
<div class="agent-input">
<el-input
v-model="agent.text"
type="textarea"
:rows="2"
resize="none"
maxlength="200"
show-word-limit
placeholder="例如:销售出库 10 个某产品,库区 A出库人张三"
@keyup.enter.native="analyzeAgentText"
/>
<el-button icon="el-icon-delete" :disabled="!agent.text && !agent.plan" @click="clearAgent">清空</el-button>
<el-button type="primary" icon="el-icon-magic-stick" :loading="agent.loading" @click="analyzeAgentText">分析</el-button>
</div>
<div v-if="agent.plan" class="agent-result">
<div class="agent-plan">
<div>
<el-tag size="small" :type="agent.plan.executable ? 'success' : 'warning'">{{ actionLabel(agent.plan.action) }}</el-tag>
<strong>{{ agent.plan.summary || agent.plan.message || '已生成操作草稿' }}</strong>
</div>
<el-button
v-if="agent.plan.needConfirm"
type="danger"
size="small"
icon="el-icon-check"
:disabled="!agent.plan.executable"
:loading="agent.executing"
@click="executeAgentPlan"
>确认执行</el-button>
</div>
<el-descriptions v-if="planItems.length" :column="3" size="small" border>
<el-descriptions-item v-for="item in planItems" :key="item.label" :label="item.label">{{ item.value }}</el-descriptions-item>
</el-descriptions>
<el-alert v-if="agent.plan.message" :title="agent.plan.message" type="info" show-icon :closable="false" />
<div v-if="agent.plan.warnings && agent.plan.warnings.length" class="agent-warnings">
<el-tag v-for="item in agent.plan.warnings" :key="item" type="warning" size="small">{{ item }}</el-tag>
</div>
<div v-if="agent.plan.candidates && agent.plan.candidates.length" class="agent-candidates">
<span v-for="item in agent.plan.candidates" :key="item.type + item.id">{{ item.name }} {{ item.detail || '' }}</span>
</div>
<el-table v-if="agent.plan.results && agent.plan.results.length" :data="agent.plan.results" size="mini" class="agent-table">
<el-table-column type="index" label="#" width="54" />
<el-table-column label="库区" prop="areaName" width="120" />
<el-table-column label="产品" min-width="220">
<template slot-scope="scope">
<div class="product-name">{{ scope.row.productName || '-' }}</div>
<div class="product-meta">{{ productMeta(scope.row) }}</div>
</template>
</el-table-column>
<el-table-column label="库存" prop="stockNum" align="right" width="110" />
</el-table>
</div>
</section>
<section class="summary-grid">
<div class="summary-card">
<span>库存品项</span>
@ -85,6 +147,7 @@ import { listStock } from '@/api/warehouse/stock'
import { listInRecord } from '@/api/warehouse/inRecord'
import { listOutRecord } from '@/api/warehouse/outRecord'
import { listStockCheck } from '@/api/warehouse/stockCheck'
import { analyzeAgent, executeAgent } from '@/api/warehouse/agent'
export default {
name: 'Index',
@ -96,13 +159,88 @@ export default {
pendingCheck: 0,
outTotal: 0
},
stockList: []
stockList: [],
agent: {
text: '',
loading: false,
executing: false,
contextResults: [],
plan: null
}
}
},
computed: {
planItems() {
const plan = this.agent.plan
if (!plan) {
return []
}
return [
{ label: '库区', value: plan.areaName },
{ label: '产品', value: plan.productName },
{ label: '品牌', value: plan.brand },
{ label: '类别', value: plan.category },
{ label: '规格', value: plan.spec },
{ label: '单位', value: plan.unit },
{ label: '数量', value: plan.quantity },
{ label: '操作人', value: plan.operatorName },
{ label: '当前库存', value: plan.currentStock },
{ label: '预计库存', value: plan.afterStock },
{ label: '单号', value: plan.recordNo }
].filter(item => item.value !== undefined && item.value !== null && item.value !== '')
}
},
created() {
this.loadDashboard()
},
methods: {
analyzeAgentText() {
const text = (this.agent.text || '').trim()
if (!text) {
this.$modal.msgWarning('请输入要操作的内容')
return
}
this.agent.loading = true
analyzeAgent({ text, contextResults: this.agent.contextResults }).then(res => {
this.agent.plan = res.data
this.agent.text = ''
if (res.data && res.data.results && res.data.results.length) {
this.agent.contextResults = res.data.results
}
}).finally(() => {
this.agent.loading = false
})
},
executeAgentPlan() {
if (!this.agent.plan || !this.agent.plan.executable) {
return
}
this.agent.executing = true
executeAgent({ plan: this.agent.plan }).then(res => {
this.agent.plan = res.data
this.$modal.msgSuccess(res.data.message || '操作完成')
this.loadDashboard()
this.agent.text = ''
}).finally(() => {
this.agent.executing = false
})
},
clearAgent() {
this.agent.text = ''
this.agent.plan = null
},
actionLabel(action) {
const map = {
query_stock: '查询库存',
create_in_record: '入库登记',
confirm_in_record: '确认入库',
create_out_record: '出库',
create_stock_check: '盘库登记',
create_product: '新增产品',
create_area: '新增库区'
}
return map[action] || '未识别'
},
loadDashboard() {
listStock({ pageNum: 1, pageSize: 8 }).then(res => {
this.stockList = res.rows || []
@ -150,6 +288,76 @@ export default {
margin: 6px 0 0;
color: #7a8794;
}
.agent-panel {
margin-bottom: 16px;
padding: 18px;
background: #ffffff;
border: 1px solid #e6ebf2;
border-radius: 8px;
}
.agent-title {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.agent-title h2 {
margin: 0;
font-size: 18px;
line-height: 26px;
color: #1f2d3d;
}
.agent-title p {
margin: 4px 0 0;
color: #7a8794;
}
.agent-input {
display: grid;
grid-template-columns: 1fr 88px 96px;
gap: 12px;
align-items: stretch;
}
.agent-input .el-button {
height: 54px;
}
.agent-result {
display: grid;
gap: 12px;
margin-top: 14px;
}
.agent-plan {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
background: #f7f9fc;
border-radius: 6px;
}
.agent-plan strong {
margin-left: 8px;
color: #1f2d3d;
}
.agent-warnings {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.agent-candidates {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.agent-candidates span {
padding: 6px 10px;
color: #606266;
background: #f4f6f9;
border-radius: 4px;
}
.agent-table {
width: 100%;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@ -222,6 +430,16 @@ export default {
align-items: flex-start;
flex-direction: column;
}
.agent-input {
grid-template-columns: 1fr;
}
.agent-input .el-button {
height: 40px;
}
.agent-plan {
align-items: flex-start;
flex-direction: column;
}
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}

View File

@ -29,6 +29,63 @@
</div>
</div>
<div class="mobile-agent">
<div class="agent-head">
<div>
<div class="agent-title">智能库存助手</div>
<div class="agent-subtitle">说一句或输入一句确认后执行</div>
</div>
<el-tag size="mini" type="info">AI</el-tag>
</div>
<el-input
v-model="agent.text"
type="textarea"
:rows="2"
resize="none"
maxlength="200"
show-word-limit
placeholder="例如:销售出库 10 个某产品,库区 A出库人张三"
/>
<div class="agent-actions">
<el-button icon="el-icon-delete" :disabled="!agent.text && !agent.plan" @click="clearAgent">清空</el-button>
<el-button type="primary" icon="el-icon-magic-stick" :loading="agent.loading" @click="analyzeAgentText">分析</el-button>
</div>
<div v-if="agent.plan" class="agent-result">
<div class="agent-plan">
<div>
<el-tag size="mini" :type="agent.plan.executable ? 'success' : 'warning'">{{ actionLabel(agent.plan.action) }}</el-tag>
<strong>{{ agent.plan.summary || agent.plan.message || '已生成操作草稿' }}</strong>
</div>
<el-button
v-if="showAgentExecute(agent.plan)"
type="danger"
size="mini"
icon="el-icon-check"
:disabled="!agent.plan.executable"
:loading="agent.executing"
@click="executeAgentPlan"
>确认</el-button>
</div>
<div v-if="agentPlanItems.length" class="agent-detail">
<span v-for="item in agentPlanItems" :key="item.label">{{ item.label }}{{ item.value }}</span>
</div>
<el-alert v-if="agent.plan.message" :title="agent.plan.message" type="info" show-icon :closable="false" />
<div v-if="agent.plan.warnings && agent.plan.warnings.length" class="agent-tags">
<el-tag v-for="item in agent.plan.warnings" :key="item" type="warning" size="mini">{{ item }}</el-tag>
</div>
<div v-if="agent.plan.candidates && agent.plan.candidates.length" class="agent-candidates">
<span v-for="item in agent.plan.candidates" :key="item.type + item.id">{{ item.name }} {{ item.detail || '' }}</span>
</div>
<article v-for="(item, index) in (agent.plan.results || [])" :key="'agent-result-' + item.id" class="agent-stock">
<div>
<div class="card-title"> {{ index + 1 }} {{ item.productName || '-' }}</div>
<div class="meta-line">{{ item.areaName || '-' }} {{ productMeta(item) }}</div>
</div>
<strong>{{ item.stockNum }}</strong>
</article>
</div>
</div>
<div class="quick-panel">
<button @click="openInForm">入库登记</button>
<button @click="openOutForm">出库登记</button>
@ -108,6 +165,8 @@
<span>入库人{{ item.inOperator || '-' }}</span>
</div>
<div class="card-actions" v-if="item.inStatus === 0">
<el-button type="primary" size="mini" icon="el-icon-edit" @click="openInForm(item)" v-hasPermi="['warehouse:in:edit']">修改</el-button>
<el-button type="danger" size="mini" icon="el-icon-delete" @click="deleteInItem(item)" v-hasPermi="['warehouse:in:remove']">删除</el-button>
<el-button type="success" size="mini" icon="el-icon-check" @click="confirmIn(item)" v-hasPermi="['warehouse:in:confirm']">确认入库</el-button>
</div>
</article>
@ -372,9 +431,10 @@ import { listArea, getArea, delArea, addArea, updateArea, optionselectArea } fro
import { listProduct, getProduct, delProduct, addProduct, updateProduct, optionselectProduct } from '@/api/warehouse/product'
import { optionselectUser } from '@/api/warehouse/user'
import { listStock, outStock } from '@/api/warehouse/stock'
import { listInRecord, addInRecord, confirmInRecord } from '@/api/warehouse/inRecord'
import { listInRecord, getInRecord, addInRecord, updateInRecord, delInRecord, confirmInRecord } from '@/api/warehouse/inRecord'
import { listOutRecord, addOutRecord } from '@/api/warehouse/outRecord'
import { listStockCheck, addStockCheck, confirmStockCheck } from '@/api/warehouse/stockCheck'
import { analyzeAgent, executeAgent } from '@/api/warehouse/agent'
export default {
name: 'MobileWarehouse',
@ -402,7 +462,15 @@ export default {
dialogType: '',
dialogTitle: '',
form: {},
formRules: {}
formRules: {},
agent: {
text: '',
loading: false,
executing: false,
contextResults: [],
pendingNextPlan: null,
plan: null
}
}
},
computed: {
@ -415,6 +483,25 @@ export default {
},
unitOptions() {
return this.distinctProductOptions('unit')
},
agentPlanItems() {
const plan = this.agent.plan
if (!plan) {
return []
}
return [
{ label: '库区', value: plan.areaName },
{ label: '产品', value: plan.productName },
{ label: '品牌', value: plan.brand },
{ label: '类别', value: plan.category },
{ label: '规格', value: plan.spec },
{ label: '单位', value: plan.unit },
{ label: '数量', value: plan.quantity },
{ label: '人员', value: plan.operatorName },
{ label: '当前', value: plan.currentStock },
{ label: '预计', value: plan.afterStock },
{ label: '单号', value: plan.recordNo }
].filter(item => item.value !== undefined && item.value !== null && item.value !== '')
}
},
created() {
@ -422,6 +509,158 @@ export default {
this.refreshAll()
},
methods: {
analyzeAgentText() {
const text = (this.agent.text || '').trim()
if (!text) {
this.$modal.msgWarning('请输入要操作的内容')
return
}
this.agent.loading = true
analyzeAgent({ text, contextResults: this.agent.contextResults }).then(res => {
this.agent.plan = res.data
this.agent.text = ''
if (res.data && res.data.results && res.data.results.length) {
this.agent.contextResults = res.data.results
}
this.autoFillAgentForm(res.data)
}).finally(() => {
this.agent.loading = false
})
},
executeAgentPlan() {
if (!this.agent.plan || !this.agent.plan.executable) {
return
}
this.agent.executing = true
executeAgent({ plan: this.agent.plan }).then(res => {
this.agent.plan = res.data
this.$modal.msgSuccess(res.data.message || '操作完成')
this.refreshAll()
this.agent.text = ''
}).finally(() => {
this.agent.executing = false
})
},
clearAgent() {
this.agent.text = ''
this.agent.plan = null
this.agent.pendingNextPlan = null
},
autoFillAgentForm(plan) {
if (!plan || !plan.executable) {
return
}
if (plan.action === 'create_out_record' && plan.stockId) {
this.dialogType = 'stockOut'
this.dialogTitle = '库存出库'
this.form = {
id: plan.stockId,
productName: plan.productName,
brand: plan.brand,
category: plan.category,
spec: plan.spec,
unit: plan.unit,
outOperator: plan.operatorName,
outType: plan.outType || 3,
outNum: plan.quantity,
sourceNo: undefined,
remark: plan.remark
}
this.setOutRules()
} else if (plan.action === 'create_out_record') {
this.dialogType = 'out'
this.dialogTitle = '出库登记'
this.form = {
areaId: plan.areaId,
productId: plan.productId,
outOperator: plan.operatorName,
outType: plan.outType || 3,
outNum: plan.quantity,
sourceNo: undefined,
remark: plan.remark
}
this.setOutRules()
} else if (plan.action === 'create_in_record') {
this.dialogType = 'in'
this.dialogTitle = '入库登记'
this.form = {
areaId: plan.areaId,
productId: plan.productId,
inOperator: plan.operatorName,
inType: plan.inType || 2,
inNum: plan.quantity,
remark: plan.remark
}
this.setInRules()
} else if (plan.action === 'create_stock_check') {
this.dialogType = 'check'
this.dialogTitle = '盘库登记'
this.form = {
areaId: plan.areaId,
productId: plan.productId,
realNum: plan.quantity,
reason: plan.remark
}
this.setCheckRules()
} else if (plan.action === 'create_product') {
this.dialogType = 'product'
this.dialogTitle = '新增产品'
this.agent.pendingNextPlan = plan.nextPlan || null
this.form = {
id: undefined,
productCode: undefined,
productName: plan.productName,
brand: plan.brand,
category: plan.category,
spec: plan.spec,
unit: plan.unit,
remark: plan.remark
}
this.formRules = {
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }]
}
} else if (plan.action === 'create_area') {
this.dialogType = 'area'
this.dialogTitle = '新增库区'
this.agent.pendingNextPlan = plan.nextPlan || null
this.form = {
id: undefined,
areaCode: undefined,
areaName: plan.areaName,
remark: plan.remark
}
this.formRules = {
areaName: [{ required: true, message: '请输入库区名称', trigger: 'blur' }]
}
} else {
return
}
this.dialogOpen = true
this.$nextTick(() => {
if (this.$refs.mobileForm) {
this.$refs.mobileForm.clearValidate()
}
})
this.$modal.msgSuccess('已自动填好表单,请确认后提交')
},
showAgentExecute(plan) {
if (!plan || !plan.needConfirm) {
return false
}
return !['create_out_record', 'create_in_record', 'create_stock_check', 'create_product', 'create_area'].includes(plan.action)
},
actionLabel(action) {
const map = {
query_stock: '查库存',
create_in_record: '入库',
confirm_in_record: '确认入库',
create_out_record: '出库',
create_stock_check: '盘库',
create_product: '新增产品',
create_area: '新增库区'
}
return map[action] || '未识别'
},
loadOptions() {
optionselectArea().then(res => { this.areaOptions = res.data || [] })
optionselectProduct().then(res => { this.productOptions = res.data || [] })
@ -489,10 +728,31 @@ export default {
const values = this.productOptions.map(item => item[field]).filter(item => item !== undefined && item !== null && item !== '')
return Array.from(new Set(values))
},
openInForm() {
openInForm(row) {
this.dialogType = 'in'
this.dialogTitle = '入库登记'
this.form = { areaId: undefined, productId: undefined, inOperator: undefined, inType: 1, inNum: undefined, remark: undefined }
this.setInRules()
const setForm = data => {
this.dialogTitle = data && data.id ? '修改入库登记' : '入库登记'
this.form = Object.assign({
id: undefined,
inNo: undefined,
areaId: undefined,
productId: undefined,
inOperator: undefined,
inType: 1,
inNum: undefined,
remark: undefined
}, data || {})
this.dialogOpen = true
this.$nextTick(() => this.resetForm('mobileForm'))
}
if (row && row.id) {
getInRecord(row.id).then(response => setForm(response.data))
} else {
setForm()
}
},
setInRules() {
this.formRules = {
areaId: [{ required: true, message: '请选择库区', trigger: 'change' }],
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
@ -500,8 +760,6 @@ export default {
inType: [{ required: true, message: '请选择类型', trigger: 'change' }],
inNum: [{ required: true, message: '请输入数量', trigger: 'blur' }]
}
this.dialogOpen = true
this.$nextTick(() => this.resetForm('mobileForm'))
},
openOutForm() {
this.dialogType = 'out'
@ -532,13 +790,62 @@ export default {
this.dialogType = 'check'
this.dialogTitle = '盘库登记'
this.form = { areaId: undefined, productId: undefined, realNum: undefined, reason: undefined }
this.setCheckRules()
this.dialogOpen = true
this.$nextTick(() => this.resetForm('mobileForm'))
},
setCheckRules() {
this.formRules = {
areaId: [{ required: true, message: '请选择库区', trigger: 'change' }],
productId: [{ required: true, message: '请选择产品', trigger: 'change' }],
realNum: [{ required: true, message: '请输入实盘数量', trigger: 'blur' }]
}
this.dialogOpen = true
this.$nextTick(() => this.resetForm('mobileForm'))
},
openPendingAgentPlanAfterProduct() {
const nextPlan = this.agent.pendingNextPlan
const savedProduct = Object.assign({}, this.form)
this.agent.pendingNextPlan = null
optionselectProduct().then(res => {
this.productOptions = res.data || []
const product = this.productOptions.find(item =>
this.sameText(item.productName, savedProduct.productName) &&
this.sameText(item.brand, savedProduct.brand) &&
this.sameText(item.category, savedProduct.category) &&
this.sameText(item.spec, savedProduct.spec)
)
if (!product) {
this.$modal.msgWarning('产品已保存,请手动选择产品完成入库')
return
}
nextPlan.productId = product.id
nextPlan.productName = product.productName
nextPlan.brand = product.brand
nextPlan.category = product.category
nextPlan.spec = product.spec
nextPlan.unit = product.unit
nextPlan.executable = true
this.$nextTick(() => this.autoFillAgentForm(nextPlan))
})
},
openPendingAgentPlanAfterArea() {
const nextPlan = this.agent.pendingNextPlan
const savedArea = Object.assign({}, this.form)
this.agent.pendingNextPlan = null
optionselectArea().then(res => {
this.areaOptions = res.data || []
const area = this.areaOptions.find(item => this.sameText(item.areaName, savedArea.areaName))
if (!area) {
this.$modal.msgWarning('库区已保存,请手动选择库区完成操作')
return
}
nextPlan.areaId = area.id
nextPlan.areaName = area.areaName
nextPlan.executable = true
this.$nextTick(() => this.autoFillAgentForm(nextPlan))
})
},
sameText(left, right) {
return String(left || '').trim().toLowerCase() === String(right || '').trim().toLowerCase()
},
openProductForm(row) {
this.dialogType = 'product'
@ -592,7 +899,7 @@ export default {
this.$refs.mobileForm.validate(valid => {
if (!valid) return
let request
if (this.dialogType === 'in') request = addInRecord(this.form)
if (this.dialogType === 'in') request = this.form.id ? updateInRecord(this.form) : addInRecord(this.form)
if (this.dialogType === 'out') request = addOutRecord(this.form)
if (this.dialogType === 'stockOut') request = outStock(this.form)
if (this.dialogType === 'check') request = addStockCheck(this.form)
@ -600,6 +907,7 @@ export default {
if (this.dialogType === 'area') request = this.form.id ? updateArea(this.form) : addArea(this.form)
request.then(() => {
const editMaintainData = (this.dialogType === 'product' || this.dialogType === 'area') && this.form.id
const hasPendingNextPlan = !!this.agent.pendingNextPlan
this.$modal.msgSuccess(editMaintainData ? '修改成功' : '保存成功')
this.dialogOpen = false
if (this.dialogType === 'product') {
@ -607,6 +915,11 @@ export default {
this.loadOptions()
this.loadStock()
this.loadSummary()
if (hasPendingNextPlan && !editMaintainData) {
this.openPendingAgentPlanAfterProduct()
} else {
this.clearAgent()
}
return
}
if (this.dialogType === 'area') {
@ -614,8 +927,14 @@ export default {
this.loadOptions()
this.loadStock()
this.loadSummary()
if (hasPendingNextPlan && !editMaintainData) {
this.openPendingAgentPlanAfterArea()
} else {
this.clearAgent()
}
return
}
this.clearAgent()
this.refreshAll()
})
})
@ -638,6 +957,13 @@ export default {
this.loadSummary()
}).catch(() => {})
},
deleteInItem(row) {
this.$modal.confirm('确认删除入库记录“' + (row.inNo || '-') + '”?').then(() => delInRecord(row.id)).then(() => {
this.$modal.msgSuccess('删除成功')
this.loadInRecords()
this.loadSummary()
}).catch(() => {})
},
confirmIn(row) {
this.$modal.confirm('确认将该记录入库?').then(() => confirmInRecord(row.id)).then(() => {
this.$modal.msgSuccess('入库成功')
@ -715,6 +1041,98 @@ export default {
.summary-card.danger strong {
color: #f56c6c;
}
.mobile-agent {
margin: 12px 0;
padding: 12px;
background: #ffffff;
border: 1px solid #e8edf3;
border-radius: 8px;
}
.agent-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
}
.agent-title {
font-size: 16px;
font-weight: 700;
line-height: 22px;
}
.agent-subtitle {
color: #7a8794;
font-size: 12px;
line-height: 18px;
}
.agent-actions {
display: grid;
grid-template-columns: 86px 1fr;
gap: 8px;
margin-top: 8px;
}
.agent-result {
display: grid;
gap: 10px;
margin-top: 10px;
}
.agent-plan {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
padding: 10px;
background: #f7f9fc;
border-radius: 6px;
}
.agent-plan strong {
display: block;
margin-top: 5px;
line-height: 20px;
}
.agent-detail {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.agent-detail span {
padding: 5px 8px;
color: #606266;
background: #f4f6f9;
border-radius: 4px;
font-size: 12px;
}
.agent-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.agent-candidates {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.agent-candidates span {
padding: 5px 8px;
color: #606266;
background: #f4f6f9;
border-radius: 4px;
font-size: 12px;
}
.agent-stock {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
padding: 10px;
border: 1px solid #e8edf3;
border-radius: 6px;
}
.agent-stock strong {
color: #1f7aec;
font-size: 18px;
white-space: nowrap;
}
.quick-panel {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@ -874,4 +1292,19 @@ export default {
.mobile-dialog .el-dialog__body {
padding: 12px 16px 0;
}
@media (max-width: 768px) {
.el-message-box__wrapper {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
box-sizing: border-box;
}
.el-message-box {
width: 100% !important;
max-width: 340px;
margin-top: 0 !important;
margin-bottom: 0;
}
}
</style>