修复前端API路径与后端RESTful路由不匹配:升级mock插件支持参数化路由,修正6处API URL

main
haoliang 1 week ago
parent 2065bf75c5
commit d7fb9fc2b1

@ -39,6 +39,17 @@ const mock: MockMethod[] = [
response: () => ({ code: 0, data: { items: brands } }),
},
{
// 参数化路由:匹配 /mock-api/admin/brand/1, /mock-api/admin/brand/2 等
url: '/mock-api/admin/brand/:id',
method: 'get',
response: ({ params }: any) => {
const id = Number(params.id)
const b = brands.find((b: any) => b.id === id)
return { code: 0, data: { ...b, mappings: id === 1 ? fanucMappings : [] } }
},
},
{
// 兼容旧URL格式
url: '/mock-api/admin/brand/detail',
method: 'get',
response: ({ query }: any) => {
@ -63,6 +74,13 @@ const mock: MockMethod[] = [
response: () => ({ code: 0, message: 'success', data: null }),
},
{
// 参数化路由POST /mock-api/admin/brand/:id/copy
url: '/mock-api/admin/brand/:id/copy',
method: 'post',
response: () => ({ code: 0, message: 'success', data: { id: 4, brandName: '新复制品牌' } }),
},
{
// 兼容旧URL格式
url: '/mock-api/admin/brand/copy',
method: 'post',
response: () => ({ code: 0, message: 'success', data: { id: 4, brandName: '新复制品牌' } }),

@ -8,20 +8,39 @@ const addresses = [
const mock: MockMethod[] = [
{ url: '/mock-api/admin/collect-address', method: 'get', response: () => ({ code: 0, data: { items: addresses } }) },
// 参数化路由GET /mock-api/admin/collect-address/:id
{ url: '/mock-api/admin/collect-address/:id', method: 'get', response: ({ params }: any) => { const id = Number(params.id); const a = addresses.find((a: any) => a.id === id); return { code: 0, data: a || null } } },
// 兼容旧URL格式
{ url: '/mock-api/admin/collect-address/detail', method: 'get', response: ({ query }: any) => { const id = Number(query.id); const a = addresses.find((a: any) => a.id === id); return { code: 0, data: a || null } } },
{ url: '/mock-api/admin/collect-address', method: 'post', response: () => ({ code: 0, message: 'success', data: { id: 4, name: '新地址' } }) },
{ url: '/mock-api/admin/collect-address/update', method: 'post', response: () => ({ code: 0, message: 'success', data: null }) },
{ url: '/mock-api/admin/collect-address/delete', method: 'post', response: () => ({ code: 0, message: 'success', data: null }) },
{ url: '/mock-api/admin/collect-address/toggle', method: 'post', response: () => ({ code: 0, message: 'success', data: null }) },
// 参数化路由GET /mock-api/admin/collect-address/:id/machines
{ url: '/mock-api/admin/collect-address/:id/machines', method: 'get', response: () => ({ code: 0, data: { items: [
{ machineId: 1, machineName: '西-1.8', deviceCode: 'fanake_1.8', workshopName: 'A栋', isOnline: 1, programName: '1566.NC', partCount: 580 },
{ machineId: 2, machineName: '西-1.10', deviceCode: 'fanake_1.10', workshopName: 'A栋', isOnline: 1, programName: 'O123.NC', partCount: 120 },
] } }) },
// 兼容旧URL格式
{ url: '/mock-api/admin/collect-address/machines', method: 'get', response: () => ({ code: 0, data: { items: [
{ machineId: 1, machineName: '西-1.8', deviceCode: 'fanake_1.8', workshopName: 'A栋', isOnline: 1, programName: '1566.NC', partCount: 580 },
{ machineId: 2, machineName: '西-1.10', deviceCode: 'fanake_1.10', workshopName: 'A栋', isOnline: 1, programName: 'O123.NC', partCount: 120 },
] } }) },
// 参数化路由GET /mock-api/admin/collect-address/:id/collect-records
{ url: '/mock-api/admin/collect-address/:id/collect-records', method: 'get', response: () => ({ code: 0, data: { items: [
{ requestTime: '2026-04-25T17:36:38', duration: 1200, isSuccess: 1, machineCount: 32, failMachineCount: 0, machineName: '西-1.8' },
{ requestTime: '2026-04-25T17:35:38', duration: 3500, isSuccess: 1, machineCount: 31, failMachineCount: 1, machineName: '西-1.10' },
{ requestTime: '2026-04-25T17:35:08', duration: null, isSuccess: 0, machineCount: null, failMachineCount: null, machineName: '西-1.12' },
] } }) },
// 兼容旧URL格式
{ url: '/mock-api/admin/collect-address/collect-records', method: 'get', response: () => ({ code: 0, data: { items: [
{ requestTime: '2026-04-25T17:36:38', duration: 1200, isSuccess: 1, machineCount: 32, failMachineCount: 0, machineName: '西-1.8' },
{ requestTime: '2026-04-25T17:35:38', duration: 3500, isSuccess: 1, machineCount: 31, failMachineCount: 1, machineName: '西-1.10' },
{ requestTime: '2026-04-25T17:35:08', duration: null, isSuccess: 0, machineCount: null, failMachineCount: null, machineName: '西-1.12' },
] } }) },
// 参数化路由GET /mock-api/admin/collect-address/:id/raw-json
{ url: '/mock-api/admin/collect-address/:id/raw-json', method: 'get', response: () => ({ code: 0, data: { rawJson: '[{device:fanake_1.8,tags:[{id:Tag5,value:1566.NC}]}]' } }) },
// 兼容旧URL格式
{ url: '/mock-api/admin/collect-address/raw-json', method: 'get', response: () => ({ code: 0, data: { rawJson: '[{device:fanake_1.8,tags:[{id:Tag5,value:1566.NC}]}]' } }) },
{ url: '/mock-api/admin/brand/list', method: 'get', response: () => ({ code: 0, data: { items: [{ id: 1, brandName: 'FANUC' }, { id: 2, brandName: 'SIEMENS' }] } }) },
{ url: '/mock-api/admin/machine/list', method: 'get', response: ({ query }: any) => {

@ -17,11 +17,23 @@ const mock: MockMethod[] = [
if (query.workshopId) items = items.filter((m: any) => m.workshopId === Number(query.workshopId))
if (query.isOnline !== undefined && query.isOnline !== '') items = items.filter((m: any) => m.isOnline === Number(query.isOnline))
if (query.brandId) items = items.filter((m: any) => m.brandId === Number(query.brandId))
if (query.brandName) items = items.filter((m: any) => m.brandName === query.brandName)
if (query.keyword) items = items.filter((m: any) => m.name.includes(query.keyword) || m.deviceCode.includes(query.keyword))
return { code: 0, data: { items, total: items.length, page: Number(query.page || 1), pageSize: Number(query.pageSize || 20) } }
},
},
{
// 参数化路由GET /mock-api/admin/machine/:id
url: '/mock-api/admin/machine/:id',
method: 'get',
response: ({ params }: any) => {
const id = Number(params.id)
const m = machines.find((m: any) => m.id === id)
return { code: 0, data: m || null }
},
},
{
// 兼容旧URL格式
url: '/mock-api/admin/machine/detail',
method: 'get',
response: ({ query }: any) => {
@ -31,6 +43,16 @@ const mock: MockMethod[] = [
},
},
{
// 参数化路由GET /mock-api/admin/machine/:id/status
url: '/mock-api/admin/machine/:id/status',
method: 'get',
response: () => ({
code: 0,
data: { programName: '1566.NC', partCount: 580, runStatus: '运行中', operateMode: '自动', spindleSpeedSet: 3000, feedSpeedSet: 500, spindleSpeedActual: 2980, feedSpeedActual: 480, spindleLoad: 65, machiningStatus: 'G01', lastCollectTime: '2026-04-25T17:36:38' },
}),
},
{
// 兼容旧URL格式
url: '/mock-api/admin/machine/status',
method: 'get',
response: () => ({
@ -59,6 +81,19 @@ const mock: MockMethod[] = [
response: () => ({ code: 0, message: 'success', data: null }),
},
{
// 参数化路由GET /mock-api/admin/machine/:id/production/today
url: '/mock-api/admin/machine/:id/production/today',
method: 'get',
response: () => ({
code: 0,
data: { items: [
{ programName: '1566.NC', quantity: 580, runTime: '4h20m', cuttingTime: '3h50m' },
{ programName: 'O123.NC', quantity: 120, runTime: '2h10m', cuttingTime: '1h45m' },
] },
}),
},
{
// 兼容旧URL格式
url: '/mock-api/admin/machine/production/today',
method: 'get',
response: () => ({
@ -70,6 +105,21 @@ const mock: MockMethod[] = [
}),
},
{
// 参数化路由GET /mock-api/admin/machine/:id/production/trend
url: '/mock-api/admin/machine/:id/production/trend',
method: 'get',
response: () => ({
code: 0,
data: { items: [
{ date: '2026-04-19', quantity: 820 }, { date: '2026-04-20', quantity: 760 },
{ date: '2026-04-21', quantity: 910 }, { date: '2026-04-22', quantity: 850 },
{ date: '2026-04-23', quantity: 780 }, { date: '2026-04-24', quantity: 900 },
{ date: '2026-04-25', quantity: 700 },
] },
}),
},
{
// 兼容旧URL格式
url: '/mock-api/admin/machine/production/trend',
method: 'get',
response: () => ({
@ -83,6 +133,20 @@ const mock: MockMethod[] = [
}),
},
{
// 参数化路由GET /mock-api/admin/machine/:id/collect-records
url: '/mock-api/admin/machine/:id/collect-records',
method: 'get',
response: () => ({
code: 0,
data: { items: [
{ collectTime: '2026-04-25T17:36:35', programName: '1566.NC', partCount: 580, runStatus: '运行中' },
{ collectTime: '2026-04-25T17:36:05', programName: '1566.NC', partCount: 579, runStatus: '运行中' },
{ collectTime: '2026-04-25T17:35:35', programName: '1566.NC', partCount: 578, runStatus: '运行中' },
] },
}),
},
{
// 兼容旧URL格式
url: '/mock-api/admin/machine/collect-records',
method: 'get',
response: () => ({

@ -71,6 +71,26 @@ export function viteMockPlugin(options: MockPluginOptions): Plugin {
}
})
// 参数化路由匹配:支持 /mock-api/admin/brand/:id 格式
function matchRoute(pattern: string, path: string): { matched: boolean; params: Record<string, string> } {
// 无参数占位符时精确匹配
if (!pattern.includes(':')) {
return { matched: pattern === path, params: {} }
}
// 将 :param 转为正则捕获组
const paramNames: string[] = []
const regexStr = pattern.replace(/:([^/]+)/g, (_, name) => {
paramNames.push(name)
return '([^/]+)'
})
const regex = new RegExp('^' + regexStr + '$')
const match = path.match(regex)
if (!match) return { matched: false, params: {} }
const params: Record<string, string> = {}
paramNames.forEach((name, i) => { params[name] = match[i + 1] })
return { matched: true, params }
}
server.middlewares.use(async (req, res, next) => {
const routes = await loadMockRoutes()
@ -79,12 +99,18 @@ export function viteMockPlugin(options: MockPluginOptions): Plugin {
const parsedUrl = new URL(req.url || '/', 'http://localhost')
const reqPath = parsedUrl.pathname
// 匹配路由URL 精确匹配 + method 匹配
const matched = routes.find(r => {
if (r.url !== reqPath) return false
if (!r.method) return true
return r.method.toLowerCase() === reqMethod
})
// 匹配路由:支持精确匹配 + 参数化路由 + method 匹配
// 参数化路由优先级低于精确匹配,遍历时先匹配到的优先
let matched: MockMethod | null = null
let matchedParams: Record<string, string> = {}
for (const r of routes) {
const { matched: isMatched, params } = matchRoute(r.url, reqPath)
if (!isMatched) continue
if (r.method && r.method.toLowerCase() !== reqMethod) continue
matched = r
matchedParams = params
break
}
if (!matched) return next()
@ -113,7 +139,7 @@ export function viteMockPlugin(options: MockPluginOptions): Plugin {
// 执行 response
let responseBody: any
if (typeof matched.response === 'function') {
const mockReq: MockRequest = { query, body, url: req.url || '', method: reqMethod }
const mockReq: MockRequest = { query, body, url: req.url || '', method: reqMethod, params: matchedParams }
responseBody = matched.response(mockReq)
} else {
responseBody = matched.response

@ -7,6 +7,8 @@ export interface MockRequest {
body: any
url: string
method: string
/** URL路径参数如 /brand/:id 中的 { id: '1' } */
params: Record<string, string>
}
export interface MockMethod {

@ -20,6 +20,12 @@ const mock: MockMethod[] = [
items = items.slice(start, end)
return { code: 0, data: { items, total, page, pageSize } }
}},
// 参数化路由GET /mock-api/admin/worker/:id
{ url: '/mock-api/admin/worker/:id', method: 'get', response: ({ params }: any) => {
const w = workers.find((w: any) => w.id === Number(params.id))
return { code: 0, data: w || null }
}},
// 兼容旧URL格式
{ url: '/mock-api/admin/worker/detail', method: 'get', response: ({ query }: any) => {
const w = workers.find((w: any) => w.id === Number(query.id))
return { code: 0, data: w || null }
@ -28,14 +34,34 @@ const mock: MockMethod[] = [
{ url: '/mock-api/admin/worker/update', method: 'post', response: () => ({ code: 0, message: 'success', data: null }) },
{ url: '/mock-api/admin/worker/delete', method: 'post', response: () => ({ code: 0, message: 'success', data: null }) },
{ url: '/mock-api/admin/worker/batch-status', method: 'post', response: () => ({ code: 0, message: 'success', data: null }) },
// 参数化路由GET /mock-api/admin/worker/:id/machines
{ url: '/mock-api/admin/worker/:id/machines', method: 'get', response: () => ({ code: 0, data: { items: [
{ machineId: 1, machineName: '西-1.8', deviceCode: 'fanake_1.8', workshopName: 'A栋', brandName: 'FANUC', isOnline: 1, programName: '1566.NC' },
{ machineId: 2, machineName: '西-2.0', deviceCode: 'fanake_2.0', workshopName: 'A栋', brandName: 'FANUC', isOnline: 0, programName: null },
] } }) },
// 兼容旧URL格式
{ url: '/mock-api/admin/worker/machines', method: 'get', response: () => ({ code: 0, data: { items: [
{ machineId: 1, machineName: '西-1.8', deviceCode: 'fanake_1.8', workshopName: 'A栋', brandName: 'FANUC', isOnline: 1, programName: '1566.NC' },
{ machineId: 2, machineName: '西-2.0', deviceCode: 'fanake_2.0', workshopName: 'A栋', brandName: 'FANUC', isOnline: 0, programName: null },
] } }) },
// 参数化路由GET /mock-api/admin/worker/:id/production/today
{ url: '/mock-api/admin/worker/:id/production/today', method: 'get', response: () => ({ code: 0, data: { items: [
{ machineName: '西-1.8', programName: '1566.NC', quantity: 580, runTime: '4h20m', cuttingTime: '3h50m' },
{ machineName: '西-2.0', programName: '-', quantity: null, runTime: '-', cuttingTime: '-' },
] } }) },
// 兼容旧URL格式
{ url: '/mock-api/admin/worker/production/today', method: 'get', response: () => ({ code: 0, data: { items: [
{ machineName: '西-1.8', programName: '1566.NC', quantity: 580, runTime: '4h20m', cuttingTime: '3h50m' },
{ machineName: '西-2.0', programName: '-', quantity: null, runTime: '-', cuttingTime: '-' },
] } }) },
// 参数化路由GET /mock-api/admin/worker/:id/production/trend
{ url: '/mock-api/admin/worker/:id/production/trend', method: 'get', response: () => ({ code: 0, data: { items: [
{ date: '2026-04-19', quantity: 980 }, { date: '2026-04-20', quantity: 920 },
{ date: '2026-04-21', quantity: 1100 }, { date: '2026-04-22', quantity: 1050 },
{ date: '2026-04-23', quantity: 990 }, { date: '2026-04-24', quantity: 1080 },
{ date: '2026-04-25', quantity: 580 },
] } }) },
// 兼容旧URL格式
{ url: '/mock-api/admin/worker/production/trend', method: 'get', response: () => ({ code: 0, data: { items: [
{ date: '2026-04-19', quantity: 980 }, { date: '2026-04-20', quantity: 920 },
{ date: '2026-04-21', quantity: 1100 }, { date: '2026-04-22', quantity: 1050 },

@ -44,7 +44,7 @@ const form = reactive({ brandName: '', deviceField: 'device', tagsPath: 'tags',
function addMapping() { form.mappings.push({ standardField: '', fieldName: '', matchBy: 'id', dataType: 'string', isRequired: 0 }) }
async function loadData() {
if (!isEdit) return
const r = await request.get<Brand>('/admin/brand/detail', { params: { id: route.params.id } })
const r = await request.get<Brand>(`/admin/brand/${route.params.id}`)
if (r.data) { form.brandName = r.data.brandName; form.deviceField = r.data.deviceField; form.tagsPath = r.data.tagsPath; form.mappings = (r.data as any).mappings || [] }
}
async function handleSave() {

@ -39,7 +39,7 @@ async function handleDelete(row: Brand) { await ElMessageBox.confirm('确定删
async function handleCopy(row: Brand) {
try {
await ElMessageBox.confirm(`确定复制品牌【${row.brandName}】?`, '提示', { type: 'warning' })
await request.post('/admin/brand/copy', { sourceId: row.id })
await request.post(`/admin/brand/${row.id}/copy`)
ElMessage.success('复制成功')
loadData()
} catch {

@ -53,7 +53,7 @@ const form=reactive({name:'',url:'',brandId:undefined as number|undefined,collec
//
const machineList=ref<{ machineId:number; machineName:string }[]>([])
//
async function loadMachinesForBrandName(brandName:string){ if(!brandName){ machineList.value = []; return } const r: ApiResponse<{ items: { machineId:number; machineName:string }[] }> = await request.get('/admin/machine/list', { params: { brandName } }); machineList.value = r.data?.items || [] }
async function loadMachinesForBrandName(brandName:string){ if(!brandName){ machineList.value = []; return } const r: ApiResponse<{ items: { machineId:number; machineName:string }[] }> = await request.get('/admin/machine', { params: { brandName } }); machineList.value = r.data?.items || [] }
function resetQuery(){query.brandId=undefined;loadData()}
function goDetail(id:number){router.push((isMock.value?'/mock/collect-address/':'/collect-address/')+id)}
async function loadData(){loading.value=true;try{const r:any=await request.get('/admin/collect-address');tableData.value=r.data?.items||[]}finally{loading.value=false}}
@ -61,7 +61,7 @@ function handleAdd(){editingId.value=null;Object.assign(form,{name:'',url:'',bra
function handleEdit(row:any){editingId.value=row.id;Object.assign(form,row);dialogVisible.value=true}
async function handleSubmit(){submitting.value=true;try{await request[editingId.value?'put':'post'](editingId.value?`/admin/collect-address/${editingId.value}`:'/admin/collect-address',{...form});ElMessage.success('保存成功');dialogVisible.value=false;loadData()}finally{submitting.value=false}}
async function handleDelete(row:any){await ElMessageBox.confirm('确定删除【'+row.name+'】?此操作不可恢复。','提示',{type:'warning'});await request.delete(`/admin/collect-address/${row.id}`);ElMessage.success('已删除');loadData()}
async function loadDrops(){const r:any=await request.get('/admin/brand/list');brandList.value=r.data?.items||[]}
async function loadDrops(){const r:any=await request.get('/admin/brand');brandList.value=r.data?.items||[]}
onMounted(()=>{loadData();loadDrops()})
//
watch(() => form.brandId, async (newVal)=>{

@ -293,9 +293,9 @@ async function handleImport() {
async function loadDrops() {
const w: ApiResponse<{ items: Workshop[] }> = await request.get('/admin/workshop')
const b: ApiResponse<{ items: Brand[] }> = await request.get('/admin/brand/list')
const b: ApiResponse<{ items: Brand[] }> = await request.get('/admin/brand')
const a: ApiResponse<{ items: CollectAddress[] }> = await request.get('/admin/collect-address')
const wk: ApiResponse<{ items: Worker[] }> = await request.get('/admin/worker/list')
const wk: ApiResponse<{ items: Worker[] }> = await request.get('/admin/worker')
workshopList.value = w.data?.items ?? []
brandList.value = b.data?.items ?? []
addressList.value = a.data?.items ?? []

@ -100,7 +100,7 @@ function handleEdit(row: Worker){editingId.value=row.id;Object.assign(form,{code
async function handleSubmit(){submitting.value=true;try{const ok = await (workerForm.value?.validate ? new Promise<boolean>((resolve)=>workerForm.value!.validate((valid:boolean)=>resolve(valid))) : Promise.resolve(true)); if(!ok){return} await request[editingId.value?'put':'post'](editingId.value?`/admin/worker/${editingId.value}`:'/admin/worker',{...form});ElMessage.success('保存成功');dialogVisible.value=false;loadData()}finally{submitting.value=false}}
async function handleDelete(row:any){await ElMessageBox.confirm('确定删除【'+row.name+'】?此操作不可恢复。','提示',{type:'warning'});await request.delete(`/admin/worker/${row.id}`);ElMessage.success('已删除');loadData()}
async function batchStatus(isEnabled:number){await ElMessageBox.confirm('确定对选中的'+selectedRows.value.length+'项操作?','提示',{type:'warning'});for(const id of selectedRows.value.map((r:any)=>r.id)){await request.put(`/admin/worker/${id}/toggle`,{isEnabled})};ElMessage.success('操作成功');loadData()}
async function loadDrops(){const r: ApiResponse<{ items: Array<{ id: number; name: string; deviceCode?: string; workshopName?: string }> }> = await request.get('/admin/worker/available-machines'); availableMachines.value = (r.data?.items ?? []).map(m => ({ id: m.id, name: m.name, label: `${m.deviceCode || m.name} (${m.name})${m.workshopName ? ' - ' + m.workshopName : ''}` }))}
async function loadDrops(){try{const r: ApiResponse<{ items: Array<{ id: number; name: string; deviceCode?: string; workshopName?: string }> }> = await request.get('/admin/worker/available-machines'); availableMachines.value = (r.data?.items ?? []).map(m => ({ id: m.id, name: m.name, label: `${m.deviceCode || m.name} (${m.name})${m.workshopName ? ' - ' + m.workshopName : ''}` }))}catch{/* 接口不可用时保持为空,不影响其他功能 */}}
function handlePageChange(page:number){pagination.value.currentPage=page;loadData()}
function handleSizeChange(size:number){pagination.value.pageSize=size;pagination.value.currentPage=1;loadData()}
onMounted(()=>{loadData();loadDrops()})

Loading…
Cancel
Save