vuetemplatedivclasspageRouterLink to/orders← 返回列表/RouterLinkh1订单详情/h1pclasshintparamscode{{params}}/code多接口预取 → data.detail/data.logs/pp v-ifstatus loadingclassstatus加载中…/pp v-else-ifstatus errorclassstatus error失败{{error?.message}}/ptemplate v-else-ifstatus success datael-descriptions:column1border title详情 (data.detail)el-descriptions-item labelparams[1]{{params[1]}}/el-descriptions-itemel-descriptions-item labelparams[2]{{params[2]}}/el-descriptions-itemel-descriptions-item label订单 ID{{data.detail?.id}}/el-descriptions-itemel-descriptions-item label名称{{data.detail?.name}}/el-descriptions-itemel-descriptions-item label金额{{data.detail?.amount}}/el-descriptions-itemel-descriptions-item label状态{{data.detail?.status}}/el-descriptions-itemel-descriptions-item label买家{{data.detail?.buyer}}/el-descriptions-itemel-descriptions-item label备注{{data.detail?.remark}}/el-descriptions-item/el-descriptionsh3classsub日志(data.logs)/h3el-table:datadata.logs?.list || []border stripeel-table-column proptimelabel时间width100/el-table-column proptextlabel内容//el-table/templatenavRouterLink:to/orders/${params[1]}/infoinfo/RouterLinkRouterLink:to/orders/${params[1]}/paypay/RouterLinkRouterLink:to/orders/${params[1]}/loglog/RouterLink/nav/div/templatescript setupimport{toRouterDataRefs}from../utils/routePrefetchconst{data,error,status,params,apis}toRouterDataRefs()console.log(data.value,params.value,apis.value)// 刷新await apis.getOrder(params[1]) / await apis.getOrderLogs(params[1])/scriptstyle scoped.page{padding:24px;font-family:system-ui,sans-serif;max-width:720px;}.hint,.status{color:#64748b;margin:12px0;}.hint code{color:#0f172a;background:#f1f5f9;padding:2px 6px;border-radius:4px;}.error{color:#dc2626;}.sub{margin:24px012px;font-size:16px;}nav{display:flex;gap:16px;margin-top:24px;}a{color:#2563eb;}/styleroute.jsimport{createRouter,createWebHistory}fromvue-routerimport{prefetchRoute}from../utils/routePrefetchimportHomefrom../views/Home.vueimportUserfrom../views/User.vueimportOrdersfrom../views/Orders.vueimportOrderDetailfrom../views/OrderDetail.vueconstroutercreateRouter({history:createWebHistory(),routes:[{path:/,name:Home,component:Home},{path:/user/:id,name:User,component:User,meta:{loader:true},},{path:/orders,name:Orders,component:Orders,meta:{loader:true},},{path:/orders/:id/:type,name:OrderDetail,component:OrderDetail,meta:{loader:true},},],})router.beforeEach((to,_from,next){next()prefetchRoute(to)})exportdefaultrouterbeforeRoute同步请求/** * 路由跳转时并行预取数据 → 写入 routerData模块级不依赖 setup * * 配置 loaders本文件下方 * apis — 页面后续调用翻页/刷新 * prefetch — 首次进入 Promise.all 执行结果写入 data[key] * * 页面 * const { data, error, status, params, apis } toRouterDataRefs() */import{reactive,readonly,toRefs}fromvueimport{getUser}from../api/userimport{getOrders,getOrder,getOrderLogs}from../api/orders/** 路由 path → 预取配置 */exportconstloaders{/user/:id:{apis:{getUser},prefetch:{detail:(to,signal)getUser(to.params.id,{signal}),},},/orders:{apis:{getOrders},prefetch:{list:(_to,signal)getOrders({pageNum:1,pageSize:10},{signal}),},},/orders/:id/:type:{apis:{getOrder,getOrderLogs},prefetch:{detail:(to,signal)getOrder(to.params.id,{signal}),logs:(to,signal)getOrderLogs(to.params.id,{signal}),},},}/** * param {import(vue-router).RouteLocationNormalized} to * returns {{ apis: Recordstring, Function, prefetch: Recordstring, Function } | null} */exportfunctionresolveLoaderEntry(to){for(letito.matched.length-1;i0;i--){constrecordto.matched[i]if(record.meta?.loader!true)continueconstentryloaders[record.path]if(!entry||typeofentry!object)continueif(!entry.prefetch||typeofentry.prefetch!object)continuereturn{apis:entry.apistypeofentry.apisobject?entry.apis:{},prefetch:entry.prefetch,}}returnnull}/** * param {import(vue-router).RouteLocationNormalized} to */exportfunctionpickRouteParams(to){/** type {Recordnumber, any} */constresult{}letindex1constseennewSet()for(constrecordofto.matched){constnamesrecord.path.match(/:([^/()?])/g)||[]for(consttokenofnames){constnametoken.slice(1)if(seen.has(name))continueseen.add(name)if(nameinto.params){result[index]to.params[name]}}}for(constvalueofObject.values(to.query)){result[index]value}returnresult}/** * param {Recordstring, Function} prefetchMap * param {import(vue-router).RouteLocationNormalized} to * param {AbortSignal} signal */asyncfunctionrunPrefetchAll(prefetchMap,to,signal){constkeysObject.keys(prefetchMap)constresultsawaitPromise.all(keys.map((key)prefetchMap[key](to,signal)),)/** type {Recordstring, any} */constdata{}keys.forEach((key,i){data[key]results[i]})returndata}/** typedef {loading | success | error} RouteDataStatus */conststatereactive({key:,/** type {RouteDataStatus} */status:/** type {RouteDataStatus} */(success),/** type {Recordstring, any | null} */data:null,/** type {Error | null} */error:null,/** type {Recordnumber, any} */params:{},/** type {Recordstring, Function} */apis:{},})exportconstrouterDatareadonly(state)/** const { data, status, error, params, apis } toRouterDataRefs() */exportfunctiontoRouterDataRefs(){returntoRefs(state)}/** type {AbortController | null} */letcurrentCtrlnull/** type {string} */letcurrentKeyfunctionresetState(key,params{}){if(currentCtrl){currentCtrl.abort()currentCtrlnull}currentKeykey state.keykey state.statussuccessstate.datanullstate.errornullstate.paramsparams state.apis{}}/** * param {import(vue-router).RouteLocationNormalized} to */exportasyncfunctionprefetchRoute(to){constentryresolveLoaderEntry(to)constkeyto.fullPathconstparamspickRouteParams(to)if(!entry){resetState(key,params)return}if(currentCtrl)currentCtrl.abort()constctrlnewAbortController()currentCtrlctrl currentKeykey state.keykey state.paramsparams state.apisentry.apis state.statusloadingstate.errornullstate.datanulltry{constdataawaitrunPrefetchAll(entry.prefetch,to,ctrl.signal)if(currentKey!key||ctrl.signal.aborted)returnstate.datadata state.statussuccessstate.errornull}catch(e){if(ctrl.signal.aborted||currentKey!key)returnif(e?.nameAbortError)returnstate.statuserrorstate.erroreinstanceofError?e:newError(String(e))}}/** param {string} [key] */exportfunctionclearRouterData(key){resetState(key,{})}