You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

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.jsdevices 导出符号保持现有 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: @srcimport.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/, ''),
      },
    },
  },
})

TDesignResolverunplugin-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/*.jssrc/stores/modules/{user,devices,ui}Store.js
  • Move: src/api/request.jssrc/utils/http.js
  • Move: 每个 src/views/Foo.vuesrc/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.jsimport 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/http
  • App.vue./store/ui@/stores/modules/uiStore
  • router/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.jsAppShellMainContainer
  • 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/httpuseUserStoreuseUiStore、现有 /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.js token 与图层模板

  • 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)
}

onUnmountedmap.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/requestAppShellviews/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"

执行方式

计划写完后请用户选择:

  1. 本会话直接按 Task 执行(executing-plans)
  2. 新会话 / 子代理按 Task 推进(subagent-driven-development)

未完成 Task 1~6 前,不要开始业务图层或批量 TDesign 化业务页。