18 KiB
laic-frontend 目录对齐与地图壳 Implementation Plan
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: 将 laic-frontend 目录/命名对齐 uav-edu-web,按需接入 TDesign 并重写主壳+登录,挂上开发环境天地图全局地图壳;业务页只搬家与修路径,功能不变。
Architecture: 一次大爆炸:先搭 Vite/@/依赖与空目录骨架 → 搬迁 views/stores/http 并修全量 import → 用 TDesign 重写 MainContainer/SideMenu/TopBar/LoginView → 以 maptalks + 天地图 TileLayer/GroupTileLayer 实现常驻 MapLayer。地图单例经 commonRefs + mapHelper 暴露;路由结构保持「/login 独立 + 登录后 MainContainer 子路由」。
Tech Stack: Vue 3、Vite 5、Pinia、Vue Router 4、Axios、tdesign-vue-next(按需)、less、maptalks、天地图瓦片
Spec: docs/superpowers/specs/2026-08-19-laic-frontend-structure-align-design.md
Global Constraints
- 开发环境底图:天地图;不引入 Mapbox /
maptalks.mapboxgl - TDesign:按需引入,禁止
app.use(TDesign)全量 - 移除未使用依赖:
element-plus、@element-plus/icons-vue - 鉴权 meta(
admin/userOnly)、HTTP 拦截器语义、业务页功能本轮不变 jcWeb/、public/不动- store 文件迁到
stores/modules/*Store.js;devices导出符号保持现有useDeviceStore(避免无谓全局改名) - 每完成一个 Task:
npm run build必须通过(Task 1 可在装依赖后先 build 旧树)
File Map
| 文件 | 职责 |
|---|---|
vite.config.js |
@ alias、envPrefix、代理保留、TDesign 按需插件 |
package.json |
增删依赖 |
.env.development |
APP_TIANDITU_TOKEN 等 |
src/utils/http.js |
原 api/request.js |
src/utils/commonRefs.js |
地图等异步单例引用 |
src/utils/setupTDesign.js |
仅当不用 unplugin 时的手写注册;优先 unplugin 则可省略 |
src/stores/index.js |
createPinia + re-export |
src/stores/modules/{user,devices,ui}Store.js |
原 store |
src/config/map.js |
天地图 token / 图层模板 / center/zoom |
src/config/urls.js |
API 常量骨架(本轮可先空或少量) |
src/core/mapHelper.js |
setMap/getMap/toggleMapMode/replace |
src/layout/MainContainer.vue |
TDesign 壳 + MapLayer + RouterView |
src/layout/MapLayer.vue |
maptalks + 天地图底图 |
src/layout/components/SideMenu.vue |
角色菜单 |
src/layout/components/TopBar.vue |
顶栏(移动菜单/标题区) |
src/views/*View/*View.vue |
原 views 搬迁;Login 重写 |
src/router/index.js |
懒加载路径 + MainContainer |
src/main.js / src/App.vue |
启动与全局 toast/confirm |
本仓库无 lint/test scripts;验收以 npm run build + 手工 npm run dev 为主。
Task 1: 依赖、Vite 与环境骨架
Files:
- Modify:
package.json - Modify:
vite.config.js - Create:
.env.development - Create:
src/config/map.js(可先最小导出) - Create:
src/config/urls.js(空骨架即可)
Interfaces:
-
Produces:
@→src;import.meta.env.APP_TIANDITU_TOKEN;TDesign 按需解析能力 -
Step 1: 安装/移除依赖
npm uninstall element-plus @element-plus/icons-vue
npm install tdesign-vue-next maptalks less
npm install -D unplugin-vue-components unplugin-auto-import
- Step 2: 改
vite.config.js
要求:
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { TDesignResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [TDesignResolver({ library: 'vue-next' })],
}),
Components({
resolvers: [TDesignResolver({ library: 'vue-next' })],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
envPrefix: ['APP_', 'VITE_'],
server: {
host: '0.0.0.0',
port: 5173,
proxy: {
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},
})
若 TDesignResolver 从 unplugin-vue-components/resolvers 导出不可用,改从 tdesign-vue-next/es 官方文档当前推荐路径引入,并在本 Task commit message 注明实际路径。
- Step 3: 写
.env.development
APP_TIANDITU_TOKEN=
APP_MAP_CENTER_LNG=106.62341584179686
APP_MAP_CENTER_LAT=26.657826154258505
APP_MAP_ZOOM=5
- Step 4: 写
src/config/map.js最小版
const token = import.meta.env.APP_TIANDITU_TOKEN || import.meta.env.VITE_TIANDITU_TOKEN || ''
export default {
token,
center: [
Number(import.meta.env.APP_MAP_CENTER_LNG) || 106.62341584179686,
Number(import.meta.env.APP_MAP_CENTER_LAT) || 26.657826154258505,
],
zoom: Number(import.meta.env.APP_MAP_ZOOM) || 5,
}
/** 天地图影像 + 注记(WGS84 lk 系列) */
export function createTiandituLayerOptions(tk) {
return {
img: {
urlTemplate: `https://t{s}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
},
cia: {
urlTemplate: `https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`,
subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],
},
}
}
- Step 5: 写
src/config/urls.js骨架
export const BASE_URL = '/api'
// 后续从调用点抽全;本轮允许仅占位
- Step 6: Build 冒烟
Run: npm run build
Expected: 成功(此时目录尚未搬迁,旧代码仍可编过)
- Step 7: Commit
git add package.json package-lock.json vite.config.js .env.development src/config/map.js src/config/urls.js
git commit -m "chore: add TDesign/maptalks tooling and map config skeleton"
Task 2: 搬迁 stores / http / views 并修 import
Files:
- Create:
src/stores/index.js - Move:
src/store/*.js→src/stores/modules/{user,devices,ui}Store.js - Move:
src/api/request.js→src/utils/http.js - Move: 每个
src/views/Foo.vue→src/views/FooView/FooView.vue(对照 spec 映射表) - Modify: 所有引用上述路径的文件(router、views、components、App.vue、http 自身)
- Delete: 空的
src/store/、src/api/与旧扁平 views
Interfaces:
-
Produces:
useUserStore/useDeviceStore/useUiStore从@/stores或@/stores/modules/*可导入 -
Produces:
import request from '@/utils/http'(或 default export 名保持request) -
Consumes: 无行为变更
-
Step 1: 建
stores并搬家
mkdir -p src/stores/modules
git mv src/store/user.js src/stores/modules/userStore.js
git mv src/store/devices.js src/stores/modules/devicesStore.js
git mv src/store/ui.js src/stores/modules/uiStore.js
devicesStore.js 内 import request from '@/utils/http'(可先相对路径,Step 3 统一)。
写 src/stores/index.js:
import { createPinia } from 'pinia'
export * from './modules/userStore'
export * from './modules/devicesStore'
export * from './modules/uiStore'
export function setupStore(app) {
app.use(createPinia())
}
- Step 2: 迁 http
mkdir -p src/utils
git mv src/api/request.js src/utils/http.js
更新 http.js 内 store/router import 为 @/stores/modules/userStore 等;default export 保持不变。
- Step 3: 迁全部 views
按 spec 表逐个 git mv,例如:
mkdir -p src/views/LoginView && git mv src/views/Login.vue src/views/LoginView/LoginView.vue
# …其余 13 个同样处理
完整列表见 spec「文件搬迁对照」。
- Step 4: 全仓改 import / 路由懒加载
必须替换:
../store/user→@/stores/modules/userStore(或@/stores)../store/devices→@/stores/modules/devicesStore../store/ui→@/stores/modules/uiStore../api/request/./api/request→@/utils/httpApp.vue中./store/ui→@/stores/modules/uiStorerouter/index.js中所有import('../views/Xxx.vue')→import('../views/XxxView/XxxView.vue')- 组件相对路径:
DockEditModal等按新位置修正
main.js 改为:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { setupStore } from './stores'
import './styles/prototype.css'
const app = createApp(App)
setupStore(app)
app.use(router)
app.mount('#app')
- Step 5: 删除空目录确认
确认无残留:src/store/、src/api/、扁平 src/views/*.vue。
- Step 6: Build
Run: npm run build
Expected: 成功;若失败,修断链 import 直至通过。
- Step 7: Commit
git add -A src
git commit -m "refactor: align stores/views/http paths with uav-edu-web layout"
Task 3: TDesign 主壳(MainContainer / SideMenu / TopBar)
Files:
- Create:
src/layout/MainContainer.vue - Create:
src/layout/components/SideMenu.vue - Create:
src/layout/components/TopBar.vue - Modify:
src/router/index.js(AppShell→MainContainer) - Delete:
src/layout/AppShell.vue - Modify: 壳层样式(Less CSS Modules)
Interfaces:
- Produces: 登录后壳层菜单分流与现 AppShell 一致(见下方菜单契约)
- Consumes:
useUserStore/useUiStore/ router names
菜单契约(行为对齐 AppShell):
-
公共:实时监控
monitor、设备管理devices -
非 admin:任务管理(plans/records/routes)、媒体中心(live/videos)、告警
alarms、账户account -
admin:用户
users、固件firmwares、系统system -
底栏:用户名首字母、退出(走
ui.confirm+userStore.logout) -
Step 1: 实现
SideMenu.vue
用 t-menu(或等价按需组件)实现上述契约;子路由 query(view=plans|records|routes|live|videos)保持与旧壳一致。样式:lang="less" module="s"。
- Step 2: 实现
TopBar.vue
最小:移动端打开侧栏按钮、可选当前页标题。不要阻塞主流程。
- Step 3: 实现
MainContainer.vue
结构:
<template>
<t-layout :class="s.root">
<t-aside :class="s.aside">
<SideMenu />
</t-aside>
<t-layout>
<t-header :class="s.header">
<TopBar />
</t-header>
<t-content :class="s.content">
<!-- MapLayer 在 Task 5 接入;本 Task 先留注释或空 slot -->
<router-view />
</t-content>
</t-layout>
</t-layout>
</template>
本 Task 可以先不挂 MapLayer(Task 5 再挂),但文件位置与 class 预留 content 相对定位(position: relative; height: 100%)。
- Step 4: 改路由
import MainContainer from '@/layout/MainContainer.vue'
// path: '/' component: MainContainer
删除对 AppShell 的引用与文件。
- Step 5: Build + 手工点菜单
Run: npm run build
Run: npm run dev → 登录后点 admin/非 admin 菜单各 1~2 项
Expected: 路由跳转正确;无全量 TDesign CSS 误引入导致的明显全局污染以外的构建错误。
- Step 6: Commit
git add src/layout src/router/index.js
git commit -m "feat: replace AppShell with TDesign MainContainer shell"
Task 4: LoginView 改 TDesign
Files:
- Modify:
src/views/LoginView/LoginView.vue
Interfaces:
-
Consumes:
@/utils/http、useUserStore、useUiStore、现有/v1/auth/*调用 -
Produces: 同功能登录/注册/找回;UI 为 TDesign 表单
-
Step 1: 用 t-form / t-input / t-button 重写登录主面板
保留:
- 密码登录(对接原
submitLogin) - 短信登录 UI(若原逻辑未接通后端,保持「前端未开放/同等行为」)
- 注册、找回密码流程与校验文案
可用左右分栏:左侧品牌区可暂留简化视觉;右侧 TDesign 表单。
- Step 2: 去掉对旧 login 专用 DOM class 的强依赖(壳层)
Login 全屏,不进 MainContainer;确认无地图闪现。
- Step 3: Build + 手工登录/失败提示
Run: npm run build
Expected: 登录成功进 /monitor;401/错误 toast 仍可用。
- Step 4: Commit
git add src/views/LoginView/LoginView.vue
git commit -m "feat: rewrite LoginView with on-demand TDesign"
Task 5: 地图骨架(commonRefs / mapHelper / MapLayer / 天地图)
Files:
- Create:
src/utils/commonRefs.js - Create:
src/core/mapHelper.js - Create:
src/layout/MapLayer.vue - Modify:
src/layout/MainContainer.vue(挂载 MapLayer) - Modify:
src/config/map.js(若 Task 1 需补 GroupTileLayer 工厂)
Interfaces:
-
Produces:
commonRefs.getRef('map')→Promise<maptalks.Map> -
Produces:
mapHelper.setMap/getMap/toggleMapMode/replaceMapContainer/restoreMapContainer -
Consumes:
config/map.jstoken 与图层模板 -
Step 1: 实现
commonRefs.js
对齐 edu 语义:setRef / getRef(Promise) / removeRef / clearPendingList。可用精简版,但 getRef 必须支持「地图尚未 init 时等待」。
- Step 2: 实现
mapHelper.js最小集
class MapHelper {
_map = null
_originParent = null
_savedView = null
setMap(map) { this._map = map }
getMap() { return this._map }
toggleMapMode({ is2D = false, is3D = false } = {}) {
if (!this._map) return
if (is2D) {
this._map.setPitch(0)
this._map.setBearing(0)
// 按 maptalks API 关闭/限制拖倾若有
} else if (is3D) {
this._map.setPitch(45)
}
}
replaceMapContainer(newParent) {
if (!this._map || !newParent) return
const el = this._map.getContainer()
this._originParent = el.parentNode
this._savedView = {
center: this._map.getCenter(),
zoom: this._map.getZoom(),
pitch: this._map.getPitch(),
bearing: this._map.getBearing(),
}
newParent.appendChild(el)
this._map.resize?.() || this._map.checkSize?.()
}
restoreMapContainer() {
if (!this._map || !this._originParent) return
const el = this._map.getContainer()
this._originParent.appendChild(el)
if (this._savedView) {
this._map.setCenter(this._savedView.center)
this._map.setZoom(this._savedView.zoom)
this._map.setPitch(this._savedView.pitch)
this._map.setBearing(this._savedView.bearing)
}
this._map.resize?.() || this._map.checkSize?.()
this._originParent = null
this._savedView = null
}
}
export default new MapHelper()
按实际 maptalks 版本校正 resize/checkSize 方法名。
- Step 3: 实现
MapLayer.vue
要点:
import * as maptalks from 'maptalks'
import mapConfig, { createTiandituLayerOptions } from '@/config/map'
import commonRefs from '@/utils/commonRefs'
import mapHelper from '@/core/mapHelper'
import 'maptalks/dist/maptalks.css'
function initMap() {
const tk = mapConfig.token
if (!tk) {
console.error('[MapLayer] 缺少 APP_TIANDITU_TOKEN,跳过地图初始化')
return
}
const layers = createTiandituLayerOptions(tk)
const baseLayer = new maptalks.GroupTileLayer('tianditu-base', [
new maptalks.TileLayer('tdt-img', layers.img),
new maptalks.TileLayer('tdt-cia', layers.cia),
])
const map = new maptalks.Map('map', {
center: mapConfig.center,
zoom: mapConfig.zoom,
minZoom: 3,
attribution: false,
baseLayer,
})
commonRefs.setRef('map', map)
mapHelper.setMap(map)
}
onUnmounted:map.remove()、commonRefs.removeRef('map')、mapHelper.setMap(null)。
模板:<div id="map" :class="s.root" />,绝对定位铺满 content。
- Step 4: 挂到 MainContainer
t-content 内顺序:MapLayer 在下,RouterView 在上(业务层可盖住地图)。给 content 设 position: relative; overflow: hidden; height: ...。
- Step 5: 配置 token 后手工验收
在 .env.development 填入有效天地图 key(若无 key:确认控制台错误且 SPA 其余路由可用)。
Run: npm run build
Run: npm run dev → 登录后应见影像底图;切换 /devices 再回 /monitor 不重新 new Map(可用 console 打点或 performance 观察,MapLayer 仅随 MainContainer 生命周期)。
- Step 6: Commit
git add src/utils/commonRefs.js src/core/mapHelper.js src/layout/MapLayer.vue src/layout/MainContainer.vue src/config/map.js .env.development
git commit -m "feat: add Tianditu MapLayer skeleton with mapHelper/commonRefs"
Task 6: 收尾验收与清理
Files:
-
Modify: 任何残留旧路径、未用文件
-
Optional:
README.md补一句 dev 启动与APP_TIANDITU_TOKEN(仅当仓库已有 README 或用户要求;当前无 README 则不要新建,除非用户点名) -
Step 1: 全局搜旧路径
确认无命中:src/store/、api/request、AppShell、views/Monitor.vue(扁平)、element-plus。
- Step 2: 对照 spec 验收清单
逐条勾选 spec「验收标准」。
- Step 3: 最终 build
Run: npm run build
Expected: 通过。
- Step 4: Commit(若有清理)
git add -A
git commit -m "chore: finalize structure alignment cleanup"
执行方式
计划写完后请用户选择:
- 本会话直接按 Task 执行(executing-plans)
- 新会话 / 子代理按 Task 推进(subagent-driven-development)
未完成 Task 1~6 前,不要开始业务图层或批量 TDesign 化业务页。