diff --git a/docs/superpowers/plans/2026-09-14-mms-v2.3-three-stage-implementation-plan.md b/docs/superpowers/plans/2026-09-14-mms-v2.3-three-stage-implementation-plan.md
index 4304bd81..f391d37c 100644
--- a/docs/superpowers/plans/2026-09-14-mms-v2.3-three-stage-implementation-plan.md
+++ b/docs/superpowers/plans/2026-09-14-mms-v2.3-three-stage-implementation-plan.md
@@ -33,27 +33,27 @@
当前系统具备物料、简单入库、简单出库、库存查询、物料申请、用户登录和日志页面,但与 v2.3 存在以下结构性差距:
-| 当前实现 | v2.3 目标 | 处理阶段 |
-| --------------------------------- | --------------------- | --------- |
-| 用户只有一个 `admin/user` 枚举角色 | 用户与角色多对多,权限和数据范围分别控制 | 第一阶段 |
-| 自制固定密钥 Token,权限依赖 Token 中的单角色 | 标准 JWT,按请求加载最新角色和数据范围 | 第一阶段 |
-| 物料编码全局唯一 | 同一经营组织内唯一 | 第一阶段 |
-| 分类和单位为自由文本 | 分类、物料类型、单位及换算为受控主数据 | 第一阶段 |
-| 没有业务伙伴、组织、仓库和库位实体 | 完整主数据及停用规则 | 第一阶段 |
-| 库存直接保存在 `materials.current_stock` | 第一阶段拆分独立库存余额,第二阶段启用不可变库存流水和过账内核 | 第一、二阶段 |
-| 创建或删除入出库单直接增减库存 | 草稿、提交、过账、撤销过账状态机 | 第二阶段 |
-| 没有调拨、盘点、预留和通知闭环 | 完整仓储执行闭环 | 第二阶段 |
-| 没有 BOM 和制造订单 | BOM 版本、展开、校验和制造执行 | 第三阶段 |
-| 没有 Excel 任务和 SSO | 受控批量任务及 OIDC SSO | 第三阶段 |
-| 没有自动化测试 | 后端单元/集成测试与前端组件测试 | 第一阶段起持续建设 |
+| 当前实现 | v2.3 目标 | 处理阶段 |
+| --------------------------------- | ------------------------------- | --------- |
+| 用户只有一个 `admin/user` 枚举角色 | 用户与角色多对多,权限和数据范围分别控制 | 第一阶段 |
+| 自制固定密钥 Token,权限依赖 Token 中的单角色 | 标准 JWT,按请求加载最新角色和数据范围 | 第一阶段 |
+| 物料编码全局唯一 | 同一经营组织内唯一 | 第一阶段 |
+| 分类和单位为自由文本 | 分类、物料类型、单位及换算为受控主数据 | 第一阶段 |
+| 没有业务伙伴、组织、仓库和库位实体 | 完整主数据及停用规则 | 第一阶段 |
+| 库存直接保存在 `materials.current_stock` | 第一阶段拆分独立库存余额,第二阶段启用不可变库存流水和过账内核 | 第一、二阶段 |
+| 创建或删除入出库单直接增减库存 | 草稿、提交、过账、撤销过账状态机 | 第二阶段 |
+| 没有调拨、盘点、预留和通知闭环 | 完整仓储执行闭环 | 第二阶段 |
+| 没有 BOM 和制造订单 | BOM 版本、展开、校验和制造执行 | 第三阶段 |
+| 没有 Excel 任务和 SSO | 受控批量任务及 OIDC SSO | 第三阶段 |
+| 没有自动化测试 | 后端单元/集成测试与前端组件测试 | 第一阶段起持续建设 |
## 二、三次交付边界
-| 阶段 | 交付主题 | 覆盖故事 | 可独立上线的业务结果 |
-| ---- | -------------- | ------------------------------------------------------------------------------------ | ------------------------------------------ |
+| 阶段 | 交付主题 | 覆盖故事 | 可独立上线的业务结果 |
+| ---- | -------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| 第一阶段 | 组织、权限和主数据底座 | ORG-001、CODE-001~004、MD-001~008、COM-001~005、WH-001~003、STOCK-003、AUTH-001、AUTH-003~006、AUD-001 | 用户可按多角色登录,只能访问授权组织/仓库;物料主数据与库存余额分表;可维护编码、物料、业务伙伴、仓库和库位 |
-| 第二阶段 | 仓储业务闭环 | STOCK-001~002、STOCK-004~009、NOTIFY-001 | 入库、出库、调拨、盘点、预留、撤销过账、库存流水和预警形成完整闭环 |
-| 第三阶段 | BOM、制造、批量和 SSO | BOM-001~010、BAT-001~004、AUTH-002 | 可维护受控 BOM,分批领料和完工;支持受控 Excel 任务和 OIDC 单点登录 |
+| 第二阶段 | 仓储业务闭环 | STOCK-001~002、STOCK-004~009、NOTIFY-001 | 入库、出库、调拨、盘点、预留、撤销过账、库存流水和预警形成完整闭环 |
+| 第三阶段 | BOM、制造、批量和 SSO | BOM-001~010、BAT-001~004、AUTH-002 | 可维护受控 BOM,分批领料和完工;支持受控 Excel 任务和 OIDC 单点登录 |
### 阶段依赖
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index f53d4593..bdb7394f 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -17,6 +17,7 @@ request.interceptors.request.use(
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
+ console.log('API请求:', config.method?.toUpperCase(), config.url)
return config
},
(error) => Promise.reject(error)
@@ -38,6 +39,25 @@ request.interceptors.response.use(
localStorage.removeItem('wmms_user')
router.push('/login')
ElMessage.error('登录已过期,请重新登录')
+ } else if (error.response?.status === 422) {
+ // Validation errors - parse and show friendly message
+ const data = error.response?.data
+ let msg = data?.message || ''
+ if (data?.error && typeof data.error === 'string') {
+ // Go validator errors format: "Key: 'Field' Error:Field validation ..."
+ const matches = data.error.match(/Error: (Field validation.*?)(\n|$)/gi)
+ if (matches) {
+ const fields = matches.map(m => {
+ const fieldMatch = m.match(/Key: '([^']+)'/)
+ return fieldMatch ? fieldMatch[1] : m
+ })
+ const labels = { Handler: '经手人', Supplier: '供应商', Receiver: '接收方' }
+ msg = fields.map(f => labels[f] || f).join('、') + '不能为空'
+ }
+ }
+ ElMessage.error(msg || '请检查必填字段')
+ } else if (error.response?.data?.message) {
+ ElMessage.error(error.response.data.message)
} else {
ElMessage.error(error.message || '网络错误')
}
diff --git a/frontend/src/api/materials.js b/frontend/src/api/materials.js
index f84251b3..c3d9ebf6 100644
--- a/frontend/src/api/materials.js
+++ b/frontend/src/api/materials.js
@@ -13,8 +13,11 @@ export const materialsApi = {
update(id, data) {
return request.put(`/materials/${id}`, data)
},
- delete(id) {
- return request.delete(`/materials/${id}`)
+ obsolete(id) {
+ return request.post(`/materials/${id}/obsolete`)
+ },
+ restore(id) {
+ return request.post(`/materials/${id}/restore`)
},
uploadImage(formData) {
return request.post('/upload', formData, {
diff --git a/frontend/src/api/organizations.js b/frontend/src/api/organizations.js
index 63aa0a56..ec01073b 100644
--- a/frontend/src/api/organizations.js
+++ b/frontend/src/api/organizations.js
@@ -12,11 +12,5 @@ export const organizationsApi = {
},
listRoles() {
return request.get('/roles')
- },
- listPermissions() {
- return request.get('/permissions')
- },
- updateRole(id, data) {
- return request.put(`/roles/${id}`, data)
}
}
diff --git a/frontend/src/api/stock.js b/frontend/src/api/stock.js
index 121bf4f8..90d09d7e 100644
--- a/frontend/src/api/stock.js
+++ b/frontend/src/api/stock.js
@@ -15,5 +15,11 @@ export const stockApi = {
},
getLogs(materialId, params = {}) {
return request.get(`/stock/logs/${materialId}`, { params })
+ },
+ getInventoryBalanceDetails(materialId) {
+ return request.get(`/stock/${materialId}/details`)
+ },
+ updateWarning(materialId, warningStock) {
+ return request.put(`/stock/${materialId}/warning`, { warning_stock: warningStock })
}
}
diff --git a/frontend/src/components/business/DeleteConfirm.vue b/frontend/src/components/business/DeleteConfirm.vue
index 931d1eac..830f38e4 100644
--- a/frontend/src/components/business/DeleteConfirm.vue
+++ b/frontend/src/components/business/DeleteConfirm.vue
@@ -1,9 +1,9 @@
-
+
{{ content }}
取消
- 确认删除
+ {{ confirmText }}
@@ -13,6 +13,14 @@ import { ref, watch } from 'vue'
const props = defineProps({
modelValue: Boolean,
+ title: {
+ type: String,
+ default: '确认删除'
+ },
+ confirmText: {
+ type: String,
+ default: '确认删除'
+ },
content: {
type: String,
default: '确定要删除此数据吗?此操作不可恢复。'
diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue
index 5a3db0a2..4bda20dd 100644
--- a/frontend/src/components/layout/AppSidebar.vue
+++ b/frontend/src/components/layout/AppSidebar.vue
@@ -4,12 +4,14 @@
class="app-sidebar"
@select="handleSelect"
>
-
- {{ item.label }}
+
+ {{ section.label }}
+
+ {{ item.label }}
+
+
+
+ {{ item.label }}
@@ -17,6 +19,7 @@