# WMMS Vue 重构实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 WMMS 前端从 jQuery 迁移到 Vue 3,完成 8 个页面的功能重构
**Architecture:** 前后端分离架构,Vue 3 SPA 使用 Vite 构建,Pinia 状态管理,Element Plus UI 组件库,Vue Router History 模式
**Tech Stack:** Vue 3 (Composition API) + Vite + Pinia + Vue Router 4 + Element Plus + Axios
**Spec:** docs/superpowers/specs/2026-09-07-wmms-vue-migration-design.md
---
## Global Constraints
- Vue 3.4+ (Composition API)
- Vite 5.x
- Pinia 2.x
- Vue Router 4.x
- Element Plus 2.x
- Axios 1.x
- 开发端口:8081
- 代理 API 到 localhost:8080
---
## 文件结构
```
frontend/
├── src/
│ ├── main.js # 应用入口
│ ├── App.vue # 根组件
│ ├── api/
│ │ ├── index.js # Axios 实例
│ │ ├── auth.js # 认证 API
│ │ ├── materials.js # 物料 API
│ │ ├── inbound.js # 入库 API
│ │ ├── outbound.js # 出库 API
│ │ ├── stock.js # 库存 API
│ │ ├── application.js # 申请 API
│ │ ├── logs.js # 日志 API
│ │ └── users.js # 用户 API
│ ├── stores/
│ │ ├── auth.js # 认证 Store
│ │ ├── materials.js # 物料 Store
│ │ ├── stock.js # 库存 Store
│ │ ├── inbound.js # 入库 Store
│ │ ├── outbound.js # 出库 Store
│ │ ├── application.js # 申请 Store
│ │ ├── approval.js # 审批 Store
│ │ ├── logs.js # 日志 Store
│ │ ├── users.js # 用户 Store
│ │ └── ui.js # UI 状态 Store
│ ├── router/
│ │ └── index.js # 路由配置 + 守卫
│ ├── components/
│ │ ├── layout/
│ │ │ ├── AppHeader.vue # 顶部导航
│ │ │ └── AppSidebar.vue # 侧边菜单
│ │ └── business/
│ │ ├── BaseTable.vue # 通用表格
│ │ ├── BasePagination.vue # 分页组件
│ │ ├── BaseModal.vue # 弹窗组件
│ │ ├── StatusBadge.vue # 状态徽章
│ │ ├── RoleBadge.vue # 角色徽章
│ │ ├── DeleteConfirm.vue # 删除确认
│ │ ├── AlertTip.vue # 提示消息
│ │ └── LoadingOverlay.vue # 加载遮罩
│ └── views/
│ ├── Login.vue # 登录页
│ ├── Materials.vue # 物料管理
│ ├── Inbound.vue # 入库管理
│ ├── Outbound.vue # 出库管理
│ ├── Stock.vue # 库存查询
│ ├── Application.vue # 物料申请
│ ├── Approval.vue # 申请审批
│ ├── Logs.vue # 日志记录
│ └── Users.vue # 用户管理
├── index.html
├── vite.config.js
└── package.json
```
---
## Day 1: 项目初始化 + API 层 + Pinia Store
### Task 1: 初始化 Vue 项目
**Files:**
- Create: `frontend/package.json`
- Create: `frontend/vite.config.js`
- Create: `frontend/index.html`
- Create: `frontend/src/main.js`
- Create: `frontend/src/App.vue`
**Interfaces:**
- Produces: 可运行的 Vue 3 + Vite 项目,端口 8081,代理 /api 到 localhost:8080
- [ ] **Step 1: 创建 package.json**
```json
{
"name": "wmms-frontend",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"element-plus": "^2.5.0",
"axios": "^1.6.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}
```
- [ ] **Step 2: 创建 vite.config.js**
```js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 8081,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
},
'/ws': {
target: 'ws://localhost:8080',
ws: true
}
}
}
})
```
- [ ] **Step 3: 创建 index.html**
```html
仓库物料管理系统
```
- [ ] **Step 4: 创建 main.js**
```js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
```
- [ ] **Step 5: 创建 App.vue**
```vue
```
- [ ] **Step 6: 创建基础路由文件 router/index.js**
```js
import { createRouter, createWebHistory } from 'vue-router'
const routes = []
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
```
- [ ] **Step 7: 安装依赖并验证项目运行**
Run: `cd frontend && npm install && npm run dev`
Expected: 访问 localhost:8081 显示空白页面,无报错
---
### Task 2: 创建 Axios 实例和 API 层
**Files:**
- Create: `frontend/src/api/index.js`
- Create: `frontend/src/api/auth.js`
- Create: `frontend/src/api/materials.js`
- Create: `frontend/src/api/inbound.js`
- Create: `frontend/src/api/outbound.js`
- Create: `frontend/src/api/stock.js`
- Create: `frontend/src/api/application.js`
- Create: `frontend/src/api/logs.js`
- Create: `frontend/src/api/users.js`
**Interfaces:**
- Consumes: 无
- Produces: Axios 实例,模块化 API 函数,供 Store 调用
- [ ] **Step 1: 创建 api/index.js (Axios 实例)**
```js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '../router'
const request = axios.create({
baseURL: '/api',
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('wmms_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
const res = response.data
if (res.code === 0) {
return res
}
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message))
},
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('wmms_token')
localStorage.removeItem('wmms_user')
router.push('/login')
ElMessage.error('登录已过期,请重新登录')
} else {
ElMessage.error(error.message || '网络错误')
}
return Promise.reject(error)
}
)
export default request
```
- [ ] **Step 2: 创建 api/auth.js**
```js
import request from './index'
export const authApi = {
login(username, password) {
return request.post('/auth/login', { username, password })
},
getCurrentUser() {
return request.get('/auth/me')
}
}
```
- [ ] **Step 3: 创建 api/materials.js**
```js
import request from './index'
export const materialsApi = {
getAll(params = {}) {
return request.get('/materials', { params })
},
getById(id) {
return request.get(`/materials/${id}`)
},
create(data) {
return request.post('/materials', data)
},
update(id, data) {
return request.put(`/materials/${id}`, data)
},
delete(id) {
return request.delete(`/materials/${id}`)
},
uploadImage(formData) {
return request.post('/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
}
```
- [ ] **Step 4: 创建 api/inbound.js**
```js
import request from './index'
export const inboundApi = {
getAll(params = {}) {
return request.get('/inbound', { params })
},
getById(id) {
return request.get(`/inbound/${id}`)
},
create(data) {
return request.post('/inbound', data)
},
update(id, data) {
return request.put(`/inbound/${id}`, data)
},
delete(id) {
return request.delete(`/inbound/${id}`)
}
}
```
- [ ] **Step 5: 创建 api/outbound.js**
```js
import request from './index'
export const outboundApi = {
getAll(params = {}) {
return request.get('/outbound', { params })
},
getById(id) {
return request.get(`/outbound/${id}`)
},
create(data) {
return request.post('/outbound', data)
},
update(id, data) {
return request.put(`/outbound/${id}`, data)
},
delete(id) {
return request.delete(`/outbound/${id}`)
}
}
```
- [ ] **Step 6: 创建 api/stock.js**
```js
import request from './index'
export const stockApi = {
getAll(params = {}) {
return request.get('/stock', { params })
},
getWarning(params = {}) {
return request.get('/stock/warning', { params })
},
getLogs(materialId, params = {}) {
return request.get(`/stock/logs/${materialId}`, { params })
}
}
```
- [ ] **Step 7: 创建 api/application.js**
```js
import request from './index'
export const applicationApi = {
create(data) {
return request.post('/applications', data)
},
getMy(status = '', page = 1, pageSize = 20) {
return request.get('/applications/my', {
params: { status, page, pageSize }
})
},
getAll(status = '', page = 1, pageSize = 20) {
return request.get('/applications', {
params: { status, page, pageSize }
})
},
cancel(id) {
return request.post(`/applications/${id}/cancel`, {})
},
approve(id, data) {
return request.post(`/applications/${id}/approve`, data)
},
reject(id, data) {
return request.post(`/applications/${id}/reject`, data)
}
}
```
- [ ] **Step 8: 创建 api/logs.js**
```js
import request from './index'
export const logsApi = {
getAll(params = {}) {
return request.get('/logs', { params })
}
}
```
- [ ] **Step 9: 创建 api/users.js**
```js
import request from './index'
export const usersApi = {
getAll(params = {}) {
return request.get('/users', { params })
},
create(data) {
return request.post('/users', data)
},
update(id, data) {
return request.put(`/users/${id}`, data)
},
delete(id) {
return request.delete(`/users/${id}`)
}
}
```
- [ ] **Step 10: 验证 API 层可正常调用**
Run: `cd frontend && npm run dev`,在浏览器控制台测试 API 调用
Expected: 无报错,API 能正常请求到后端
---
### Task 3: 创建 Pinia Store
**Files:**
- Create: `frontend/src/stores/auth.js`
- Create: `frontend/src/stores/materials.js`
- Create: `frontend/src/stores/stock.js`
- Create: `frontend/src/stores/inbound.js`
- Create: `frontend/src/stores/outbound.js`
- Create: `frontend/src/stores/application.js`
- Create: `frontend/src/stores/approval.js`
- Create: `frontend/src/stores/logs.js`
- Create: `frontend/src/stores/users.js`
- Create: `frontend/src/stores/ui.js`
**Interfaces:**
- Consumes: API 层函数
- Produces: 各模块的响应式状态和 actions
- [ ] **Step 1: 创建 stores/auth.js**
```js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { authApi } from '../api/auth'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('wmms_token') || '')
const user = ref(JSON.parse(localStorage.getItem('wmms_user') || 'null'))
const isLoggedIn = computed(() => !!token.value)
const isAdmin = computed(() => user.value?.role === 'admin')
function saveAuth(newToken, newUser) {
token.value = newToken
user.value = newUser
localStorage.setItem('wmms_token', newToken)
localStorage.setItem('wmms_user', JSON.stringify(newUser))
}
function clearAuth() {
token.value = ''
user.value = null
localStorage.removeItem('wmms_token')
localStorage.removeItem('wmms_user')
}
async function login(username, password) {
const res = await authApi.login(username, password)
saveAuth(res.data.token, res.data.user)
return res.data
}
async function fetchCurrentUser() {
if (!token.value) return null
try {
const res = await authApi.getCurrentUser()
user.value = res.data
return res.data
} catch {
clearAuth()
return null
}
}
function logout() {
clearAuth()
}
return {
token, user, isLoggedIn, isAdmin,
saveAuth, clearAuth, login, fetchCurrentUser, logout
}
})
```
- [ ] **Step 2: 创建 stores/materials.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { materialsApi } from '../api/materials'
export const useMaterialsStore = defineStore('materials', () => {
const materials = ref([])
const loading = ref(false)
const pagination = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
const categoryMap = {
'A01': '标准件', 'A02': '成品件', 'A03': '简易加工件', 'A04': '精密加工件',
'A05': '钣金件', 'A06': '塑胶件', 'A07': '其他',
'B01': '成品件', 'B02': '简易加工件', 'B03': 'PCB硬板', 'B04': 'FPC软板',
'B05': '线束', 'B06': '组装组件', 'B07': '其他',
'C01': '连接耗材', 'C02': '密封耗材', 'C03': '其他',
'D01': '自研程序固件', 'D02': '外购软件授权', 'D03': '授权介质', 'D04': '其他',
'E01': '成品', 'E02': '半成品',
'F01': '资料文档', 'F02': '标识标牌', 'F03': '包装物料', 'F04': '随机工具配件', 'F05': '其他'
}
function getCategoryDisplay(category) {
if (!category) return '-'
const name = categoryMap[category] || ''
return name ? `${category} - ${name}` : category
}
async function fetchMaterials(page = 1, search = '') {
loading.value = true
try {
const pageSize = pagination.value.pageSize
const offset = (page - 1) * pageSize
const res = await materialsApi.getAll({ search, limit: pageSize, offset })
materials.value = res.data?.items || res.data || []
pagination.value.total = res.data?.total || materials.value.length
pagination.value.currentPage = page
pagination.value.totalPages = Math.ceil(pagination.value.total / pageSize) || 1
} finally {
loading.value = false
}
}
async function createMaterial(data) {
const res = await materialsApi.create(data)
await fetchMaterials(1)
return res
}
async function updateMaterial(id, data) {
const res = await materialsApi.update(id, data)
await fetchMaterials(pagination.value.currentPage)
return res
}
async function deleteMaterial(id) {
const res = await materialsApi.delete(id)
await fetchMaterials(pagination.value.currentPage)
return res
}
return {
materials, loading, pagination, categoryMap,
getCategoryDisplay, fetchMaterials, createMaterial, updateMaterial, deleteMaterial
}
})
```
- [ ] **Step 3: 创建 stores/stock.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { stockApi } from '../api/stock'
export const useStockStore = defineStore('stock', () => {
const stockList = ref([])
const warningList = ref([])
const logs = ref([])
const loading = ref(false)
const stockPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
const warningPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
const logsPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
async function fetchStock(page = 1) {
loading.value = true
try {
const pageSize = stockPage.value.pageSize
const offset = (page - 1) * pageSize
const res = await stockApi.getAll({ limit: pageSize, offset })
stockList.value = res.data?.items || res.data || []
stockPage.value.total = res.data?.total || stockList.value.length
stockPage.value.currentPage = page
} finally {
loading.value = false
}
}
async function fetchStockWarning(page = 1) {
loading.value = true
try {
const pageSize = warningPage.value.pageSize
const offset = (page - 1) * pageSize
const res = await stockApi.getWarning({ limit: pageSize, offset })
warningList.value = res.data?.items || res.data || []
warningPage.value.total = res.data?.total || warningList.value.length
warningPage.value.currentPage = page
} finally {
loading.value = false
}
}
async function fetchStockLogs(materialId, page = 1) {
loading.value = true
try {
const pageSize = logsPage.value.pageSize
const offset = (page - 1) * pageSize
const res = await stockApi.getLogs(materialId, { limit: pageSize, offset })
logs.value = res.data?.items || res.data || []
logsPage.value.total = res.data?.total || logs.value.length
logsPage.value.currentPage = page
} finally {
loading.value = false
}
}
return {
stockList, warningList, logs, loading,
stockPage, warningPage, logsPage,
fetchStock, fetchStockWarning, fetchStockLogs
}
})
```
- [ ] **Step 4: 创建 stores/inbound.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { inboundApi } from '../api/inbound'
export const useInboundStore = defineStore('inbound', () => {
const inboundList = ref([])
const loading = ref(false)
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
async function fetchInboundList(pageNum = 1) {
loading.value = true
try {
const pageSize = page.value.pageSize
const offset = (pageNum - 1) * pageSize
const res = await inboundApi.getAll({ limit: pageSize, offset })
inboundList.value = res.data?.items || res.data || []
page.value.total = res.data?.total || inboundList.value.length
page.value.currentPage = pageNum
page.value.totalPages = Math.ceil(page.value.total / pageSize) || 1
} finally {
loading.value = false
}
}
async function createInbound(data) {
const res = await inboundApi.create(data)
await fetchInboundList(1)
return res
}
async function updateInbound(id, data) {
const res = await inboundApi.update(id, data)
await fetchInboundList(page.value.currentPage)
return res
}
async function deleteInbound(id) {
const res = await inboundApi.delete(id)
await fetchInboundList(page.value.currentPage)
return res
}
return {
inboundList, loading, page,
fetchInboundList, createInbound, updateInbound, deleteInbound
}
})
```
- [ ] **Step 5: 创建 stores/outbound.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { outboundApi } from '../api/outbound'
export const useOutboundStore = defineStore('outbound', () => {
const outboundList = ref([])
const loading = ref(false)
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
async function fetchOutboundList(pageNum = 1) {
loading.value = true
try {
const pageSize = page.value.pageSize
const offset = (pageNum - 1) * pageSize
const res = await outboundApi.getAll({ limit: pageSize, offset })
outboundList.value = res.data?.items || res.data || []
page.value.total = res.data?.total || outboundList.value.length
page.value.currentPage = pageNum
page.value.totalPages = Math.ceil(page.value.total / pageSize) || 1
} finally {
loading.value = false
}
}
async function createOutbound(data) {
const res = await outboundApi.create(data)
await fetchOutboundList(1)
return res
}
async function updateOutbound(id, data) {
const res = await outboundApi.update(id, data)
await fetchOutboundList(page.value.currentPage)
return res
}
async function deleteOutbound(id) {
const res = await outboundApi.delete(id)
await fetchOutboundList(page.value.currentPage)
return res
}
return {
outboundList, loading, page,
fetchOutboundList, createOutbound, updateOutbound, deleteOutbound
}
})
```
- [ ] **Step 6: 创建 stores/application.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { applicationApi } from '../api/application'
export const useApplicationStore = defineStore('application', () => {
const myApplications = ref([])
const loading = ref(false)
const myPage = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
async function fetchMyApplications(status = '', pageNum = 1) {
loading.value = true
try {
const res = await applicationApi.getMy(status, pageNum, myPage.value.pageSize)
myApplications.value = res.data?.items || res.data || []
myPage.value.total = res.data?.total || myApplications.value.length
myPage.value.currentPage = pageNum
myPage.value.totalPages = Math.ceil(myPage.value.total / myPage.value.pageSize) || 1
} finally {
loading.value = false
}
}
async function createApplication(data) {
const res = await applicationApi.create(data)
await fetchMyApplications('', 1)
return res
}
async function cancelApplication(id) {
const res = await applicationApi.cancel(id)
await fetchMyApplications('', myPage.value.currentPage)
return res
}
return {
myApplications, loading, myPage,
fetchMyApplications, createApplication, cancelApplication
}
})
```
- [ ] **Step 7: 创建 stores/approval.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { applicationApi } from '../api/application'
export const useApprovalStore = defineStore('approval', () => {
const applications = ref([])
const loading = ref(false)
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
async function fetchPendingApplications(status = '', pageNum = 1) {
loading.value = true
try {
const res = await applicationApi.getAll(status, pageNum, page.value.pageSize)
applications.value = res.data?.items || res.data || []
page.value.total = res.data?.total || applications.value.length
page.value.currentPage = pageNum
page.value.totalPages = Math.ceil(page.value.total / page.value.pageSize) || 1
} finally {
loading.value = false
}
}
async function approveApplication(id, data = {}) {
const res = await applicationApi.approve(id, data)
await fetchPendingApplications('', page.value.currentPage)
return res
}
async function rejectApplication(id, data = {}) {
const res = await applicationApi.reject(id, data)
await fetchPendingApplications('', page.value.currentPage)
return res
}
return {
applications, loading, page,
fetchPendingApplications, approveApplication, rejectApplication
}
})
```
- [ ] **Step 8: 创建 stores/logs.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { logsApi } from '../api/logs'
export const useLogsStore = defineStore('logs', () => {
const logs = ref([])
const loading = ref(false)
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
const filters = ref({ operationType: '', startDate: '', endDate: '' })
async function fetchLogs(pageNum = 1) {
loading.value = true
try {
const pageSize = page.value.pageSize
const offset = (pageNum - 1) * pageSize
const params = { limit: pageSize, offset, ...filters.value }
const res = await logsApi.getAll(params)
logs.value = res.data?.items || res.data || []
page.value.total = res.data?.total || logs.value.length
page.value.currentPage = pageNum
page.value.totalPages = Math.ceil(page.value.total / pageSize) || 1
} finally {
loading.value = false
}
}
function setFilters(newFilters) {
filters.value = { ...filters.value, ...newFilters }
}
function resetFilters() {
filters.value = { operationType: '', startDate: '', endDate: '' }
}
return {
logs, loading, page, filters,
fetchLogs, setFilters, resetFilters
}
})
```
- [ ] **Step 9: 创建 stores/users.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { usersApi } from '../api/users'
export const useUsersStore = defineStore('users', () => {
const users = ref([])
const loading = ref(false)
const page = ref({ currentPage: 1, pageSize: 20, total: 0, totalPages: 0 })
async function fetchUsers(pageNum = 1) {
loading.value = true
try {
const pageSize = page.value.pageSize
const offset = (pageNum - 1) * pageSize
const res = await usersApi.getAll({ limit: pageSize, offset })
users.value = res.data?.items || res.data || []
page.value.total = res.data?.total || users.value.length
page.value.currentPage = pageNum
page.value.totalPages = Math.ceil(page.value.total / pageSize) || 1
} finally {
loading.value = false
}
}
async function createUser(data) {
const res = await usersApi.create(data)
await fetchUsers(1)
return res
}
async function updateUser(id, data) {
const res = await usersApi.update(id, data)
await fetchUsers(page.value.currentPage)
return res
}
async function deleteUser(id) {
const res = await usersApi.delete(id)
await fetchUsers(page.value.currentPage)
return res
}
return {
users, loading, page,
fetchUsers, createUser, updateUser, deleteUser
}
})
```
- [ ] **Step 10: 创建 stores/ui.js**
```js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUiStore = defineStore('ui', () => {
const loading = ref(false)
const loadingText = ref('')
const sidebarCollapsed = ref(false)
function setLoading(visible, text = '') {
loading.value = visible
loadingText.value = text
}
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
return {
loading, loadingText, sidebarCollapsed,
setLoading, toggleSidebar
}
})
```
- [ ] **Step 11: 验证 Store 可正常工作**
Run: 在浏览器控制台测试各 Store 的 actions 是否正常
Expected: 无报错,状态正确更新
---
### Task 4: 创建布局组件和路由守卫
**Files:**
- Create: `frontend/src/components/layout/AppHeader.vue`
- Create: `frontend/src/components/layout/AppSidebar.vue`
- Modify: `frontend/src/router/index.js`
- Modify: `frontend/src/App.vue`
**Interfaces:**
- Consumes: auth Store
- Produces: 带路由守卫的完整路由配置,布局组件
- [ ] **Step 1: 创建 AppHeader.vue**
```vue
```
- [ ] **Step 2: 创建 AppSidebar.vue**
```vue
```
- [ ] **Step 3: 更新 router/index.js 添加路由和守卫**
```js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: { guestOnly: true }
},
{
path: '/',
redirect: '/stock'
},
{
path: '/materials',
name: 'Materials',
component: () => import('../views/Materials.vue'),
meta: { requiresAuth: true, adminOnly: true }
},
{
path: '/inbound',
name: 'Inbound',
component: () => import('../views/Inbound.vue'),
meta: { requiresAuth: true, adminOnly: true }
},
{
path: '/outbound',
name: 'Outbound',
component: () => import('../views/Outbound.vue'),
meta: { requiresAuth: true, adminOnly: true }
},
{
path: '/stock',
name: 'Stock',
component: () => import('../views/Stock.vue'),
meta: { requiresAuth: true }
},
{
path: '/application',
name: 'Application',
component: () => import('../views/Application.vue'),
meta: { requiresAuth: true }
},
{
path: '/approval',
name: 'Approval',
component: () => import('../views/Approval.vue'),
meta: { requiresAuth: true, adminOnly: true }
},
{
path: '/logs',
name: 'Logs',
component: () => import('../views/Logs.vue'),
meta: { requiresAuth: true, adminOnly: true }
},
{
path: '/users',
name: 'Users',
component: () => import('../views/Users.vue'),
meta: { requiresAuth: true, adminOnly: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.guestOnly && authStore.isLoggedIn) {
return next('/stock')
}
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
return next('/login')
}
if (to.meta.adminOnly && !authStore.isAdmin) {
return next('/stock')
}
next()
})
export default router
```
- [ ] **Step 4: 更新 App.vue 添加布局**
```vue
```
- [ ] **Step 5: 创建 Login.vue 基础版本**
```vue
```
- [ ] **Step 6: 验证 Day 1 完成的功能**
Run: `cd frontend && npm run dev`
Expected:
- 访问 localhost:8081 自动跳转 /login
- 登录页显示正常
- 登录成功后跳转 /stock
- 顶部导航和侧边栏显示正常
- 侧边栏根据 admin 角色显示菜单项
---
## Day 2: 基础组件 + 登录/物料/库存页面
### Task 5: 创建基础业务组件
**Files:**
- Create: `frontend/src/components/business/StatusBadge.vue`
- Create: `frontend/src/components/business/RoleBadge.vue`
- Create: `frontend/src/components/business/DeleteConfirm.vue`
- Create: `frontend/src/components/business/AlertTip.vue`
- Create: `frontend/src/components/business/LoadingOverlay.vue`
- Create: `frontend/src/components/business/BasePagination.vue`
- Create: `frontend/src/components/business/BaseTable.vue`
**Interfaces:**
- Consumes: 无
- Produces: 可复用的基础业务组件
- [ ] **Step 1: 创建 StatusBadge.vue**
```vue
{{ label }}
```
- [ ] **Step 2: 创建 RoleBadge.vue**
```vue
{{ label }}
```
- [ ] **Step 3: 创建 DeleteConfirm.vue**
```vue
{{ content }}
取消
确认删除
```
- [ ] **Step 4: 创建 AlertTip.vue**
```vue
```
- [ ] **Step 5: 创建 LoadingOverlay.vue**
```vue
```
- [ ] **Step 6: 创建 BasePagination.vue**
```vue
```
- [ ] **Step 7: 创建 BaseTable.vue**
```vue
暂无数据
```
- [ ] **Step 8: 验证基础组件可用**
Run: 在 Vue 组件中引入测试
Expected: 组件正常渲染,无报错
---
### Task 6: 创建 Materials 页面
**Files:**
- Create: `frontend/src/views/Materials.vue`
**Interfaces:**
- Consumes: materials Store, materials API
- Produces: 物料管理完整页面
- [ ] **Step 1: 创建 Materials.vue**
```vue
搜索
-
{{ materialsStore.getCategoryDisplay(row.category) }}
{{ formatDate(row.created_at) }}
编辑
删除
取消
保存
```
- [ ] **Step 2: 验证 Materials 页面**
Run: 访问 /materials
Expected: 物料列表显示正常,分页正常,搜索正常,新增/编辑/删除功能正常
---
### Task 7: 创建 Stock 页面
**Files:**
- Create: `frontend/src/views/Stock.vue`
**Interfaces:**
- Consumes: stock Store
- Produces: 库存查询、预警、日志页面
- [ ] **Step 1: 创建 Stock.vue**
```vue
库存不足
正常
{{ row.quantity }}
{{ Math.max(0, row.warning_stock - row.quantity) }}
查询
{{ formatDate(row.created_at) }}
```
- [ ] **Step 2: 验证 Stock 页面**
Run: 访问 /stock
Expected: 三个 Tab 页切换正常,库存列表、预警、日志显示正常
---
## Day 3: 出入库/申请审批/日志用户/权限
### Task 8: 创建 Inbound/Outbound 页面
**Files:**
- Create: `frontend/src/views/Inbound.vue`
- Create: `frontend/src/views/Outbound.vue`
**Interfaces:**
- Consumes: inbound Store, outbound Store, materials Store
- Produces: 入库/出库管理完整页面
- [ ] **Step 1: 创建 Inbound.vue**
```vue
{{ formatDate(row.created_at) }}
详情
编辑
删除
删除
添加物料
取消
保存
入库单号:{{ currentRow.inbound_no }}
状态:
操作人:{{ currentRow.operator }}
备注:{{ currentRow.remark || '-' }}
创建时间:{{ formatDate(currentRow.created_at) }}
```
- [ ] **Step 2: 创建 Outbound.vue(类似 Inbound.vue 结构)**
```vue
{{ formatDate(row.created_at) }}
详情
删除
库存不足
删除
添加物料
取消
保存
```
- [ ] **Step 3: 验证 Inbound/Outbound 页面**
Run: 访问 /inbound 和 /outbound
Expected: 入库/出库单列表显示正常,新增功能正常
---
### Task 9: 创建 Application/Approval/Logs/Users 页面
**Files:**
- Create: `frontend/src/views/Application.vue`
- Create: `frontend/src/views/Approval.vue`
- Create: `frontend/src/views/Logs.vue`
- Create: `frontend/src/views/Users.vue`
- [ ] **Step 1: 创建 Application.vue(物料申请表单 + 我的申请列表)**
```vue
删除
添加物料
提交申请
{{ formatDate(row.created_at) }}
取消
```
- [ ] **Step 2: 创建 Approval.vue(申请审批列表)**
```vue
{{ formatDate(row.created_at) }}
通过
拒绝
-
取消
确认通过
取消
确认拒绝
```
- [ ] **Step 3: 创建 Logs.vue(日志记录)**
```vue
查询
重置
{{ formatDate(row.created_at) }}
```
- [ ] **Step 4: 创建 Users.vue(用户管理)**
```vue
{{ formatDate(row.created_at) }}
编辑
删除
取消
保存
```
- [ ] **Step 5: 验证所有页面**
Run: 访问 /application, /approval, /logs, /users
Expected: 所有页面功能正常,权限控制生效
---
## 自检清单
### Spec 覆盖检查
- [ ] 8 个页面全部实现(Login, Materials, Inbound, Outbound, Stock, Application, Approval, Logs, Users)
- [ ] 路由守卫实现(authRequired, adminOnly, guestOnly)
- [ ] Element Plus UI 组件库使用
- [ ] Pinia 状态管理
- [ ] Axios API 层封装
- [ ] History 路由模式
### 占位符检查
- [ ] 无 "TBD"、"TODO" 等占位符
- [ ] 所有代码块完整可执行
### 类型一致性检查
- [ ] Store 的 state 和 actions 命名一致
- [ ] API 函数命名与后端接口对应
- [ ] 组件 props/emits 定义清晰
---
## 执行方式选择
**Plan complete and saved to `docs/superpowers/plans/2026-09-07-wmms-vue-migration-plan.md`.**
Two execution options:
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
**Which approach?**