Initial commit
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 wzy-warehouse
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,68 @@
|
||||
# basic_template_not_login_front
|
||||
开发基本模版——前端
|
||||
# basic_template_not_login_front
|
||||
|
||||
## 项目介绍
|
||||
`basic_template_not_login_front` 是一个包含前端的基础开发模板,旨在为快速搭建Web应用提供完整的技术栈支持。项目前端基于Vue 3 + TypeScript + Element Plus构建,可直接作为中小型Web项目的开发起点。
|
||||
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
basic_template_not_login_front
|
||||
├── public # 公共静态文件
|
||||
│ └── favicon.ico
|
||||
├── src
|
||||
│ ├── api # 前端请求后端api,所有请求都应该从这里触发
|
||||
│ │ ├── api.ts # 前端请求配置,所有请求都需要配置在这个文件中
|
||||
│ │ ├── crypto.ts # 国密加密请求
|
||||
│ ├── assets # 静态资源
|
||||
│ │ └── images
|
||||
│ │ └── logo.svg
|
||||
│ ├── config # 前端配置信息
|
||||
│ │ └── config.json # 配置
|
||||
│ ├── hooks # vue3中hooks
|
||||
│ ├── router # 路由
|
||||
│ │ └── index.ts
|
||||
│ ├── stores # pinia文件
|
||||
│ │ ├── useCryptStore.ts # 加解密pinia
|
||||
│ ├── types # 定义ts类型,理论上与后端搭配使用
|
||||
│ │ ├── crypto # 加解密相关
|
||||
│ │ │ └── Sm2PublicKeyResponse.ts # SM2公钥响应类
|
||||
│ │ └── Response.ts # 响应类,所有后端返回类型理论上应该和这个类一致
|
||||
│ ├── utils # 前端工具
|
||||
│ │ ├── request # 请求相关
|
||||
│ │ │ └── http.ts # http请求拦截,所有请求都要经过这里
|
||||
│ │ ├── safety # 安全相关
|
||||
│ │ │ └── SafetyUtils.ts # 安全配置
|
||||
│ │ └── utils.ts # 公共工具
|
||||
│ ├── views # vue3路由对应页面
|
||||
│ │ ├── home # 首页
|
||||
│ │ │ └── HomePage.vue
|
||||
│ ├── App.vue
|
||||
│ └── main.ts
|
||||
├── LICENSE # 许可证
|
||||
└── README.md # 介绍文件
|
||||
```
|
||||
|
||||
## 安装与使用
|
||||
|
||||
### 环境要求
|
||||
- Node.js 、pnpm
|
||||
|
||||
### 项目克隆
|
||||
```bash
|
||||
git clone https://github.com/wzy-warehouse/basic_template_not_login_front.git
|
||||
cd basic_template_not_login_front
|
||||
```
|
||||
|
||||
### 启动
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
### 访问系统
|
||||
+ 前端地址:[http://localhost:5173](http://localhost:5173/)
|
||||
|
||||
## 许可证
|
||||
本项目基于 [MIT License](LICENSE) 开源,详情请查看LICENSE文件。
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormatting,
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="./src/assets/images/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>basic_template_not_login_front</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "basic_template_not_login_front",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.32",
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"axios": "^1.12.2",
|
||||
"cesium": "1.101.0",
|
||||
"element-plus": "^2.11.7",
|
||||
"gm-crypto": "^0.1.12",
|
||||
"pinia": "^3.0.3",
|
||||
"spark-md5": "^3.0.2",
|
||||
"vue-router": "^4.6.3",
|
||||
"vite-plugin-cesium": "^1.2.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.12.2",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"typescript": "~6.0.2",
|
||||
"vite": "^8.0.4",
|
||||
"vue-tsc": "^3.2.6",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-vue": "~10.5.0",
|
||||
"jiti": "^2.6.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"prettier": "3.6.2",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.3"
|
||||
}
|
||||
}
|
||||
Generated
+4081
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
+15
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<RouterView></RouterView>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="App">
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: '微软雅黑';
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getSm2PublicKey } from './crypto'
|
||||
|
||||
export const $api = {
|
||||
|
||||
// 加密模块
|
||||
crypto: {
|
||||
// 获取sm2公钥
|
||||
getSm2PublicKey: () => getSm2PublicKey(),
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Sm2PublicKeyResponse } from '@/types/crypto/Sm2PublicKeyResponse'
|
||||
import type { Response } from '@/types/Response'
|
||||
import httpInstance from '@/utils/request/http'
|
||||
|
||||
/**
|
||||
* 获取sm2加密公钥
|
||||
* @returns
|
||||
*/
|
||||
export const getSm2PublicKey = (): Promise<Response<Sm2PublicKeyResponse>> => {
|
||||
return httpInstance.get('/crypto/sm2/public-key')
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1763083991244" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1008" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M896 896H554.656V256a128 128 0 0 1 128-128H896c23.552 0 42.656 19.104 42.656 42.656v682.656c0 23.552-19.104 42.656-42.656 42.656z m-426.656 0H128a42.656 42.656 0 0 1-42.656-42.656V170.688c0-23.552 19.104-42.656 42.656-42.656h213.344a128 128 0 0 1 128 128v640z m0 0h85.344v85.344h-85.344V896z" fill="#3CB956" p-id="1009"></path></svg>
|
||||
|
After Width: | Height: | Size: 664 B |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"backendBaseUrl": "http://localhost:8080",
|
||||
"apiBaseUrl": "/api",
|
||||
"noEncryptUrls": ["/crypto/sm2/public-key"],
|
||||
"tdMapToken": [
|
||||
"fc6cb1139b8eed4f79439130eb34eb00",
|
||||
"78234e018ed03fe3bb28de976dcfa6d3",
|
||||
"2e8111f9bc84149cbf24f562ed4e9229",
|
||||
"88055d3d7f13f8f7e6e8eeb67cf6d78a"
|
||||
],
|
||||
"cesiumIonDefaultAccessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI1ZDBjZjAxOS0wMDhhLTRmZjEtYjNmOC1iNmM2ZmY2ZmQ1N2IiLCJpZCI6MjAxMDI1LCJpYXQiOjE3MTAxNTgxNjJ9.mdbJYEzXQkBnHNqpozz7MvZjJ_X9a3JZRGPA-ytGhLI"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/home',
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: () => import('@/views/home/HomePage.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { type Ref, ref } from 'vue'
|
||||
|
||||
export const useCryptStore = defineStore('crypt', () => {
|
||||
// sm2公钥
|
||||
const sm2PublicKey: Ref<string> = ref('')
|
||||
|
||||
return { sm2PublicKey }
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface Response<T = unknown> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Cesium 公共配置选项
|
||||
* 用于初始化时统一配置 Viewer 参数
|
||||
*/
|
||||
export interface CesiumInitOptions {
|
||||
containerId: string // 容器 DOM ID
|
||||
terrain?: string // 地形服务地址(默认:Cesium 内置地形)
|
||||
|
||||
shouldAnimate?: boolean // 是否自动播放动画(默认:true)
|
||||
baseLayerPicker?: boolean // 是否显示图层选择器(默认:false)
|
||||
timeline?: boolean // 是否显示时间轴(默认:false)
|
||||
animation?: boolean // 是否显示动画控件(默认:false)
|
||||
infoBox?: boolean // 是否显示信息框(默认:false)
|
||||
navigationHelpButton?: boolean // 是否显示导航帮助按钮(默认:false)
|
||||
fullscreenButton?: boolean // 是否显示全屏按钮(默认:false)
|
||||
homeButton?: boolean // 是否显示主页按钮(默认:false)
|
||||
scene3DOnly?: boolean // 是否3D场景(默认:false)
|
||||
sceneModePicker?: boolean // 场景模式选择器(默认:false)
|
||||
geocoder?: boolean // 搜索(默认:false)
|
||||
|
||||
sceneMode?: number // 初始场景模式(默认:3D,可选:2D=1, COLUMBUS_VIEW=2)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Cartesian3, Color } from "cesium"
|
||||
|
||||
/**
|
||||
* 实体配置通用类型
|
||||
* 支持点、线、面、Billboard 等基础实体
|
||||
*/
|
||||
export interface EntityOptions {
|
||||
id: string // 实体唯一标识(必填,用于后续查询/删除)
|
||||
position: Cartesian3 | [number, number, number] // 位置(经纬度高程数组 或 Cartesian3)
|
||||
type: 'point' | 'polyline' | 'billboard' | 'polygon' // 实体类型
|
||||
default?: boolean // 是否是默认的实体,默认值false
|
||||
// 点配置(type='point' 时必填)
|
||||
pointOptions?: {
|
||||
color?: Color // 颜色(默认:红色)
|
||||
pixelSize?: number // 像素大小(默认:8)
|
||||
outlineColor?: Color // 轮廓颜色(默认:白色)
|
||||
outlineWidth?: number // 轮廓宽度(默认:1)
|
||||
}
|
||||
// 线配置(type='polyline' 时必填)
|
||||
polylineOptions?: {
|
||||
positions: Cartesian3[] | [number, number, number][] // 线顶点数组
|
||||
color?: Color // 颜色(默认:蓝色)
|
||||
width?: number // 线宽(默认:3)
|
||||
clampToGround?: boolean // 是否贴地(默认:false)
|
||||
}
|
||||
// Billboard 配置(type='billboard' 时必填)
|
||||
billboardOptions?: {
|
||||
image: string // 图片地址
|
||||
scale?: number // 缩放比例(默认:1)
|
||||
color?: Color // 颜色(默认:白色)
|
||||
verticalOrigin?: number // 垂直对齐方式(默认:CENTER)
|
||||
horizontalOrigin?: number // 水平对齐方式(默认:CENTER)
|
||||
}
|
||||
// 面配置(type='polygon' 时必填)
|
||||
polygonOptions?: {
|
||||
hierarchy: Cartesian3[] | [number, number, number][] // 面顶点数组
|
||||
color?: Color // 颜色(默认:绿色)
|
||||
outline?: boolean // 是否显示轮廓(默认:true)
|
||||
outlineColor?: Color // 轮廓颜色(默认:黑色)
|
||||
outlineWidth?: number // 轮廓宽度(默认:1)
|
||||
height?: number // 高度(默认:0)
|
||||
extrudedHeight?: number // extrudedHeight 高度(默认:0)
|
||||
perPositionHeight?: boolean // 是否每个顶点高度不同(默认:true)
|
||||
}
|
||||
attributes?: Record<string, unknown> // 自定义属性(用于存储额外信息)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface LayerConfig {
|
||||
id: string // 唯一id
|
||||
|
||||
type: 'imagery' | 'wms' | 'wmts' // 图层类型,支持iimagery, Geoserver WMS, Geoserver WMTS
|
||||
provider: string // 图层提供者
|
||||
url: string // 图层地址
|
||||
layers: string // 图层名称
|
||||
|
||||
default?: boolean // 是否是默认图层,默认值false
|
||||
|
||||
/**
|
||||
* WMTS 图层参数
|
||||
*/
|
||||
style?: string // 图层样式,默认为default
|
||||
format?: string // 图层格式,默认为image/png
|
||||
tileMatrixSetID?: string // 瓦片矩阵集ID,默认为EPSG:4326
|
||||
credit?: string // 图层版权信息,默认为空
|
||||
|
||||
|
||||
parameters?: Record<string, string> // 图层参数
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Cartesian3, Color } from 'cesium'
|
||||
|
||||
export interface PrimitiveOptions {
|
||||
id: string
|
||||
type: 'point' | 'polyline' | 'polygon' | 'billboard'
|
||||
positions: [number, number, number][] | Cartesian3[] // 点集合,线和面需要多个点
|
||||
default?: boolean // 是否是默认的图元,默认值false
|
||||
color?: Color
|
||||
pixelSize?: number // 点大小
|
||||
width?: number // 线宽
|
||||
image?: string // 广告牌图片
|
||||
scale?: number // 广告牌缩放
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface Sm2PublicKeyResponse {
|
||||
/**
|
||||
* 公钥
|
||||
*/
|
||||
publicKey: string
|
||||
}
|
||||
@@ -0,0 +1,935 @@
|
||||
import type { CesiumInitOptions } from '@/types/cesium/CesiumInitOptions'
|
||||
import type { EntityOptions } from '@/types/cesium/EntityOptions'
|
||||
import type { PrimitiveOptions } from '@/types/cesium/PrimitiveOptions'
|
||||
import type { LayerConfig } from '@/types/cesium/LayerConfig'
|
||||
import {
|
||||
Viewer,
|
||||
Entity,
|
||||
Cartesian3,
|
||||
Color,
|
||||
PointGraphics,
|
||||
PolylineGraphics,
|
||||
BillboardGraphics,
|
||||
SceneMode,
|
||||
CesiumTerrainProvider,
|
||||
EllipsoidTerrainProvider,
|
||||
VerticalOrigin,
|
||||
HorizontalOrigin,
|
||||
Cartographic,
|
||||
ColorMaterialProperty,
|
||||
Ion,
|
||||
WebMapTileServiceImageryProvider,
|
||||
ImageryProvider,
|
||||
ImageryLayer,
|
||||
Math as CesiumMath,
|
||||
PolygonHierarchy,
|
||||
PolygonGraphics,
|
||||
ConstantProperty,
|
||||
Primitive,
|
||||
BillboardCollection,
|
||||
GeometryInstance,
|
||||
CircleGeometry,
|
||||
ColorGeometryInstanceAttribute,
|
||||
PerInstanceColorAppearance,
|
||||
PolylineGeometry,
|
||||
PolylineColorAppearance,
|
||||
PolygonGeometry,
|
||||
ArcGisMapServerImageryProvider,
|
||||
WebMapServiceImageryProvider,
|
||||
} from 'cesium'
|
||||
import config from '@/config/config.json'
|
||||
|
||||
// 定义清除类型枚举(增强类型安全)
|
||||
export type ClearType = 'default' | 'custom' | 'all'
|
||||
|
||||
/**
|
||||
* Cesium 工具类
|
||||
* 封装 Cesium 核心操作,区分默认/自定义资源管理,支持精准增删改查
|
||||
*/
|
||||
export class CesiumUtils {
|
||||
// ===================== 实体管理(区分默认/自定义) =====================
|
||||
// 私有属性:默认实体ID集合(不希望被轻易清空的)
|
||||
#defaultEntityIds: Set<string> = new Set<string>()
|
||||
// 私有属性:自定义实体ID集合(业务添加的,可自由清空)
|
||||
#customEntityIds: Set<string> = new Set<string>()
|
||||
|
||||
// ===================== Primitive管理(区分默认/自定义) =====================
|
||||
// 私有属性:默认Primitive映射(key: ID,value: 实例)
|
||||
#defaultPrimitiveMap: Map<string, Primitive | BillboardCollection> = new Map<
|
||||
string,
|
||||
Primitive | BillboardCollection
|
||||
>()
|
||||
// 私有属性:自定义Primitive映射
|
||||
#customPrimitiveMap: Map<string, Primitive | BillboardCollection> = new Map<
|
||||
string,
|
||||
Primitive | BillboardCollection
|
||||
>()
|
||||
|
||||
// ===================== 图层管理(区分默认/自定义) =====================
|
||||
// 私有属性:默认图层映射(key: layerConfig.layers,value: 实例)
|
||||
#defaultLayerMap: Map<string, ImageryLayer> = new Map<string, ImageryLayer>()
|
||||
// 私有属性:自定义图层映射
|
||||
#customLayerMap: Map<string, ImageryLayer> = new Map<string, ImageryLayer>()
|
||||
|
||||
constructor() {
|
||||
// 初始化Cesium Ion Token
|
||||
Ion.defaultAccessToken = config.cesiumIonDefaultAccessToken
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 Cesium Viewer 实例
|
||||
* @description 封装默认配置,支持自定义扩展,返回 Viewer 实例供全局使用
|
||||
* @param options - 初始化配置项
|
||||
* @param tdMapToken - 天地图服务 token 数组
|
||||
* @param type - 底图类型:0 - 天地图「影像底图 + 影像注记」其他 - 天地图「纯矢量底图」(无注记)
|
||||
* @returns Viewer 实例
|
||||
*/
|
||||
initCesiumViewer(options: CesiumInitOptions, tdMapToken?: string[], type: number = 0): Viewer {
|
||||
// 默认天地图token
|
||||
tdMapToken = tdMapToken || config.tdMapToken
|
||||
|
||||
// 默认配置(优先级:用户传入 > 默认)
|
||||
const defaultOptions: CesiumInitOptions = {
|
||||
containerId: options.containerId,
|
||||
shouldAnimate: true,
|
||||
baseLayerPicker: false,
|
||||
timeline: false,
|
||||
animation: false,
|
||||
infoBox: false,
|
||||
navigationHelpButton: false,
|
||||
fullscreenButton: false,
|
||||
homeButton: false,
|
||||
scene3DOnly: false,
|
||||
sceneModePicker: false,
|
||||
geocoder: false,
|
||||
sceneMode: SceneMode.SCENE3D,
|
||||
}
|
||||
|
||||
const finalOptions = { ...defaultOptions, ...options }
|
||||
const container = document.getElementById(finalOptions.containerId)
|
||||
|
||||
if (!container) {
|
||||
throw new Error(`Cesium 容器 #${finalOptions.containerId} 不存在`)
|
||||
}
|
||||
|
||||
// 初始化 Viewer
|
||||
const viewer = new Viewer(container, {
|
||||
...finalOptions,
|
||||
terrainProvider: finalOptions.terrain
|
||||
? new CesiumTerrainProvider({ url: finalOptions.terrain })
|
||||
: new EllipsoidTerrainProvider(),
|
||||
|
||||
//截图和渲染相关的一些配置
|
||||
contextOptions: {
|
||||
webgl: {
|
||||
alpha: true,
|
||||
depth: false,
|
||||
stencil: true,
|
||||
antialias: true,
|
||||
premultipliedAlpha: true,
|
||||
//cesium状态下允许canvas转图片convertToImage
|
||||
preserveDrawingBuffer: true,
|
||||
failIfMajorPerformanceCaveat: true,
|
||||
},
|
||||
allowTextureFilterAnisotropic: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 优化性能:关闭不必要的渲染
|
||||
viewer.scene.globe.depthTestAgainstTerrain = false
|
||||
viewer.scene.fog.enabled = false
|
||||
viewer.scene.globe.enableLighting = false //全局光照
|
||||
viewer.shadows = false
|
||||
const creditContainer = viewer.cesiumWidget.creditContainer as HTMLElement
|
||||
creditContainer.style.display = 'none'
|
||||
|
||||
// 添加底图
|
||||
this.imageryProvider(type, tdMapToken).forEach((imageryProvider) => {
|
||||
viewer.imageryLayers.addImageryProvider(imageryProvider)
|
||||
})
|
||||
|
||||
return viewer
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加底图
|
||||
* @param type - 底图类型:0 - 天地图影像;1 - 内网自定义瓦片底图;其他 - 天地图「纯矢量底图」(无注记)
|
||||
* @param tdMapToken - 天地图token数组
|
||||
* @returns ImageryProvider 实例数组
|
||||
*/
|
||||
imageryProvider(type: number, tdMapToken: string[]): ImageryProvider[] {
|
||||
const option = {
|
||||
tileMatrixSetID: 'w',
|
||||
format: 'tiles',
|
||||
style: 'default',
|
||||
minimumLevel: 0,
|
||||
maximumLevel: 18,
|
||||
credit: 'Tianditu',
|
||||
subdomains: ['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7'],
|
||||
}
|
||||
|
||||
if (type === 0) {
|
||||
// 随机选择token避免单token超限
|
||||
const currentTokenIndex = Math.floor(Math.random() * tdMapToken.length)
|
||||
|
||||
const imageryProvider = new WebMapTileServiceImageryProvider({
|
||||
url: `https://{s}.tianditu.gov.cn/img_w/wmts?tk=${tdMapToken[currentTokenIndex]}`,
|
||||
layer: 'img',
|
||||
...option,
|
||||
})
|
||||
|
||||
const annotationProvider = new WebMapTileServiceImageryProvider({
|
||||
url: `https://{s}.tianditu.gov.cn/cia_w/wmts?tk=${tdMapToken[currentTokenIndex]}`,
|
||||
layer: 'cia',
|
||||
...option,
|
||||
})
|
||||
|
||||
return [imageryProvider, annotationProvider]
|
||||
} else {
|
||||
const vectorProvider = new WebMapTileServiceImageryProvider({
|
||||
url: `https://{s}.tianditu.gov.cn/vec_w/wmts?tk=cc`,
|
||||
layer: 'vec',
|
||||
...option,
|
||||
})
|
||||
return [vectorProvider]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加实体到场景
|
||||
* @description 统一处理点、线、Billboard 实体的创建,支持区分默认/自定义实体
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
* @param entityOptions - 实体配置项(必传 id、position、type,default标识是否为默认实体)
|
||||
* @returns 创建的 Entity 实例
|
||||
*/
|
||||
addCesiumEntity(viewer: Viewer, entityOptions: EntityOptions): Entity {
|
||||
const { id, position, type, attributes = {}, default: isDefault = false } = entityOptions
|
||||
|
||||
if (!id) throw new Error('实体 id 为必填项')
|
||||
if (!position) throw new Error('实体 position 为必填项')
|
||||
|
||||
// 校验ID唯一性(跨默认/自定义集合)
|
||||
if (this.#defaultEntityIds.has(id) || this.#customEntityIds.has(id)) {
|
||||
throw new Error(`实体 ID ${id} 已存在,请勿重复添加`)
|
||||
}
|
||||
|
||||
// 实体基础配置
|
||||
const entity = new Entity({
|
||||
id,
|
||||
position: this.convertPosition(position),
|
||||
...attributes, // 挂载自定义属性
|
||||
})
|
||||
|
||||
// 根据类型配置实体图形
|
||||
switch (type) {
|
||||
case 'point': {
|
||||
const {
|
||||
color = Color.RED,
|
||||
pixelSize = 8,
|
||||
outlineColor = Color.WHITE,
|
||||
outlineWidth = 1,
|
||||
} = entityOptions.pointOptions || {}
|
||||
entity.point = new PointGraphics({
|
||||
color,
|
||||
pixelSize,
|
||||
outlineColor,
|
||||
outlineWidth,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'polyline': {
|
||||
const {
|
||||
positions,
|
||||
color = Color.BLUE,
|
||||
width = 3,
|
||||
clampToGround = false,
|
||||
} = entityOptions.polylineOptions || {}
|
||||
if (!positions) throw new Error('线实体必须传入 polylineOptions.positions')
|
||||
|
||||
entity.polyline = new PolylineGraphics({
|
||||
positions: this.convertPositionArray(positions),
|
||||
material: new ColorMaterialProperty(color),
|
||||
width,
|
||||
clampToGround,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'billboard': {
|
||||
const {
|
||||
image,
|
||||
scale = 1,
|
||||
color = Color.WHITE,
|
||||
verticalOrigin = VerticalOrigin.CENTER,
|
||||
horizontalOrigin = HorizontalOrigin.CENTER,
|
||||
} = entityOptions.billboardOptions || {}
|
||||
if (!image) throw new Error('Billboard 实体必须传入 billboardOptions.image')
|
||||
|
||||
entity.billboard = new BillboardGraphics({
|
||||
image,
|
||||
scale,
|
||||
color,
|
||||
verticalOrigin,
|
||||
horizontalOrigin,
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'polygon': {
|
||||
const {
|
||||
hierarchy,
|
||||
color = Color.GREEN.withAlpha(0.7),
|
||||
outline = true,
|
||||
outlineColor = Color.BLACK,
|
||||
outlineWidth = 1,
|
||||
height = 0,
|
||||
extrudedHeight,
|
||||
perPositionHeight = true,
|
||||
} = entityOptions.polygonOptions || {}
|
||||
|
||||
if (!hierarchy) throw new Error('多边形实体必须传入 polygonOptions.hierarchy')
|
||||
|
||||
entity.polygon = new PolygonGraphics({
|
||||
hierarchy: this.#createConstantProperty(this.#processHierarchy(hierarchy)),
|
||||
material: new ColorMaterialProperty(color),
|
||||
outline: this.#createConstantProperty(outline),
|
||||
outlineColor: this.#createConstantProperty(outlineColor),
|
||||
outlineWidth: this.#createConstantProperty(outlineWidth),
|
||||
height: this.#createConstantProperty(height),
|
||||
extrudedHeight:
|
||||
extrudedHeight !== undefined ? this.#createConstantProperty(extrudedHeight) : undefined,
|
||||
perPositionHeight: this.#createConstantProperty(perPositionHeight),
|
||||
})
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error(`不支持的实体类型:${type}`)
|
||||
}
|
||||
|
||||
// 添加到场景并根据default标识存入对应集合
|
||||
viewer.entities.add(entity)
|
||||
if (isDefault) {
|
||||
this.#defaultEntityIds.add(id)
|
||||
} else {
|
||||
this.#customEntityIds.add(id)
|
||||
}
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 删除实体
|
||||
* @description 安全删除实体,自动识别默认/自定义并清理对应存储
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
* @param entityId - 实体 ID
|
||||
* @returns 是否删除成功
|
||||
*/
|
||||
removeCesiumEntity(viewer: Viewer, entityId: string): boolean {
|
||||
// 先判断实体类型(默认/自定义)
|
||||
const isDefault = this.#defaultEntityIds.has(entityId)
|
||||
const isCustom = this.#customEntityIds.has(entityId)
|
||||
|
||||
if (!isDefault && !isCustom) {
|
||||
console.warn(`实体 ID ${entityId} 不存在`)
|
||||
return false
|
||||
}
|
||||
|
||||
const entity = viewer.entities.getById(entityId)
|
||||
if (entity) {
|
||||
viewer.entities.remove(entity)
|
||||
// 清理对应集合
|
||||
if (isDefault) {
|
||||
this.#defaultEntityIds.delete(entityId)
|
||||
} else {
|
||||
this.#customEntityIds.delete(entityId)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除实体
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
* @param entityIds - 要删除的实体ID数组
|
||||
*/
|
||||
batchRemoveCesiumEntities(viewer: Viewer, entityIds: string[]): void {
|
||||
entityIds.forEach((id) => this.removeCesiumEntity(viewer, id))
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除实体(支持按类型筛选)
|
||||
* @description 精准清除默认/自定义/全部实体
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
* @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
|
||||
*/
|
||||
clearAllEntities(viewer: Viewer, clearType: ClearType = 'custom'): void {
|
||||
const targetIds = new Set<string>()
|
||||
|
||||
// 确定要清除的实体ID集合
|
||||
if (clearType === 'default') {
|
||||
targetIds.forEach((id) => this.#defaultEntityIds.add(id))
|
||||
} else if (clearType === 'custom') {
|
||||
this.#customEntityIds.forEach((id) => targetIds.add(id))
|
||||
} else {
|
||||
this.#defaultEntityIds.forEach((id) => targetIds.add(id))
|
||||
this.#customEntityIds.forEach((id) => targetIds.add(id))
|
||||
}
|
||||
|
||||
// 执行删除并清理对应集合
|
||||
targetIds.forEach((id) => {
|
||||
const entity = viewer.entities.getById(id)
|
||||
if (entity) viewer.entities.remove(entity)
|
||||
})
|
||||
|
||||
if (clearType === 'default') {
|
||||
this.#defaultEntityIds.clear()
|
||||
} else if (clearType === 'custom') {
|
||||
this.#customEntityIds.clear()
|
||||
} else {
|
||||
this.#defaultEntityIds.clear()
|
||||
this.#customEntityIds.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 查询实体
|
||||
* @description 安全查询实体,自动识别默认/自定义
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
* @param entityId - 实体 ID
|
||||
* @returns Entity 实例或 null
|
||||
*/
|
||||
getCesiumEntityById(viewer: Viewer, entityId: string): Entity | null {
|
||||
// 先校验是否在管理集合中
|
||||
if (!this.#defaultEntityIds.has(entityId) && !this.#customEntityIds.has(entityId)) {
|
||||
return null
|
||||
}
|
||||
return viewer.entities.getById(entityId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 定位视角到目标位置
|
||||
* @description 支持经纬度数组或 Cartesian3,可配置飞行时间
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
* @param target - 目标位置(经纬度高程数组 或 Cartesian3)
|
||||
* @param duration - 飞行时间(秒,默认:2)
|
||||
*/
|
||||
flyToTarget(viewer: Viewer, target: [number, number, number] | Cartesian3, duration = 2): void {
|
||||
const position = this.convertPosition(target)
|
||||
const cartographic = Cartographic.fromCartesian(position)
|
||||
viewer.camera.flyTo({
|
||||
destination: Cartesian3.fromDegrees(
|
||||
CesiumMath.toDegrees(cartographic.longitude),
|
||||
CesiumMath.toDegrees(cartographic.latitude),
|
||||
cartographic.height,
|
||||
),
|
||||
duration,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整视角到目标位置
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
* @param target - 目标位置(经纬度高程数组 或 Cartesian3)
|
||||
*/
|
||||
viewToTarget(viewer: Viewer, target: [number, number, number] | Cartesian3): void {
|
||||
const position = this.convertPosition(target)
|
||||
viewer.camera.setView({
|
||||
destination: position,
|
||||
orientation: {
|
||||
heading: CesiumMath.toRadians(0),
|
||||
pitch: CesiumMath.toRadians(-90),
|
||||
roll: 0.0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加Primitive类型的点线面和广告牌
|
||||
* @description 区分默认/自定义Primitive,精准管理
|
||||
* @param viewer - Cesium Viewer实例
|
||||
* @param primitives - 要添加的primitive数组(含default标识)
|
||||
*/
|
||||
addPrimitivesBatch(viewer: Viewer, primitives: PrimitiveOptions[]): void {
|
||||
// 按类型分组处理,提高渲染性能
|
||||
const pointOptions: PrimitiveOptions[] = []
|
||||
const polylineOptions: PrimitiveOptions[] = []
|
||||
const polygonOptions: PrimitiveOptions[] = []
|
||||
const billboardOptions: PrimitiveOptions[] = []
|
||||
|
||||
// 分组并校验ID唯一性(跨默认/自定义)
|
||||
primitives.forEach((option) => {
|
||||
const { id } = option
|
||||
if (this.#defaultPrimitiveMap.has(id) || this.#customPrimitiveMap.has(id)) {
|
||||
throw new Error(`Primitive ID ${id} 已存在,请勿重复添加`)
|
||||
}
|
||||
|
||||
switch (option.type) {
|
||||
case 'point':
|
||||
pointOptions.push(option)
|
||||
break
|
||||
case 'polyline':
|
||||
polylineOptions.push(option)
|
||||
break
|
||||
case 'polygon':
|
||||
polygonOptions.push(option)
|
||||
break
|
||||
case 'billboard':
|
||||
billboardOptions.push(option)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// 处理点
|
||||
if (pointOptions.length > 0) {
|
||||
const appearance = new PerInstanceColorAppearance({
|
||||
translucent: false,
|
||||
closed: true,
|
||||
})
|
||||
|
||||
const instances = pointOptions.map((option) => {
|
||||
const firstPosition = option.positions?.[0]
|
||||
if (!firstPosition) {
|
||||
throw new Error('positions 数组为空或首项缺失')
|
||||
}
|
||||
const position = this.convertPosition(firstPosition)
|
||||
return new GeometryInstance({
|
||||
id: option.id,
|
||||
geometry: new CircleGeometry({
|
||||
center: position,
|
||||
radius: option.pixelSize || 8,
|
||||
vertexFormat: appearance.vertexFormat,
|
||||
}),
|
||||
attributes: {
|
||||
color: ColorGeometryInstanceAttribute.fromColor(option.color || Color.RED),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const pointPrimitive = new Primitive({
|
||||
geometryInstances: instances,
|
||||
appearance: appearance,
|
||||
asynchronous: false,
|
||||
})
|
||||
|
||||
viewer.scene.primitives.add(pointPrimitive)
|
||||
|
||||
// 按default标识存入对应映射
|
||||
pointOptions.forEach((option) => {
|
||||
const { id, default: isDefault = false } = option
|
||||
if (isDefault) {
|
||||
this.#defaultPrimitiveMap.set(id, pointPrimitive)
|
||||
} else {
|
||||
this.#customPrimitiveMap.set(id, pointPrimitive)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理线
|
||||
if (polylineOptions.length > 0) {
|
||||
const appearance = new PolylineColorAppearance({
|
||||
translucent: true,
|
||||
})
|
||||
|
||||
const instances = polylineOptions.map((option) => {
|
||||
const positions = this.convertPositionArray(option.positions)
|
||||
return new GeometryInstance({
|
||||
id: option.id,
|
||||
geometry: new PolylineGeometry({
|
||||
positions,
|
||||
width: option.width || 3,
|
||||
vertexFormat: appearance.vertexFormat,
|
||||
}),
|
||||
attributes: {
|
||||
color: ColorGeometryInstanceAttribute.fromColor(option.color || Color.BLUE),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const polylinePrimitive = new Primitive({
|
||||
geometryInstances: instances,
|
||||
appearance: new PolylineColorAppearance({
|
||||
translucent: true,
|
||||
}),
|
||||
asynchronous: false,
|
||||
})
|
||||
|
||||
viewer.scene.primitives.add(polylinePrimitive)
|
||||
|
||||
// 按default标识存入对应映射
|
||||
polylineOptions.forEach((option) => {
|
||||
const { id, default: isDefault = false } = option
|
||||
if (isDefault) {
|
||||
this.#defaultPrimitiveMap.set(id, polylinePrimitive)
|
||||
} else {
|
||||
this.#customPrimitiveMap.set(id, polylinePrimitive)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理面
|
||||
if (polygonOptions.length > 0) {
|
||||
const appearance = new PerInstanceColorAppearance({
|
||||
translucent: true,
|
||||
closed: true,
|
||||
})
|
||||
|
||||
const instances = polygonOptions.map((option) => {
|
||||
const positions = this.convertPositionArray(option.positions)
|
||||
return new GeometryInstance({
|
||||
id: option.id,
|
||||
geometry: new PolygonGeometry({
|
||||
polygonHierarchy: new PolygonHierarchy(positions),
|
||||
vertexFormat: appearance.vertexFormat,
|
||||
}),
|
||||
attributes: {
|
||||
color: ColorGeometryInstanceAttribute.fromColor(
|
||||
option.color || Color.GREEN.withAlpha(0.5),
|
||||
),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const polygonPrimitive = new Primitive({
|
||||
geometryInstances: instances,
|
||||
appearance: new PerInstanceColorAppearance({
|
||||
translucent: true,
|
||||
closed: true,
|
||||
}),
|
||||
asynchronous: false,
|
||||
})
|
||||
|
||||
viewer.scene.primitives.add(polygonPrimitive)
|
||||
|
||||
// 按default标识存入对应映射
|
||||
polygonOptions.forEach((option) => {
|
||||
const { id, default: isDefault = false } = option
|
||||
if (isDefault) {
|
||||
this.#defaultPrimitiveMap.set(id, polygonPrimitive)
|
||||
} else {
|
||||
this.#customPrimitiveMap.set(id, polygonPrimitive)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理广告牌
|
||||
if (billboardOptions.length > 0) {
|
||||
const billboardCollection = new BillboardCollection()
|
||||
|
||||
billboardOptions.forEach((option) => {
|
||||
const firstPosition = option.positions?.[0]
|
||||
if (!firstPosition) {
|
||||
throw new Error('positions 数组为空或第一个元素未定义')
|
||||
}
|
||||
const position = this.convertPosition(firstPosition)
|
||||
billboardCollection.add({
|
||||
id: option.id,
|
||||
position,
|
||||
image: option.image,
|
||||
scale: option.scale || 1,
|
||||
color: option.color || Color.WHITE,
|
||||
})
|
||||
})
|
||||
|
||||
viewer.scene.primitives.add(billboardCollection)
|
||||
|
||||
// 按default标识存入对应映射
|
||||
billboardOptions.forEach((option) => {
|
||||
const { id, default: isDefault = false } = option
|
||||
if (isDefault) {
|
||||
this.#defaultPrimitiveMap.set(id, billboardCollection)
|
||||
} else {
|
||||
this.#customPrimitiveMap.set(id, billboardCollection)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取Primitive实例
|
||||
* @description 自动识别默认/自定义Primitive
|
||||
* @param id - Primitive的ID
|
||||
* @returns 对应的Primitive或BillboardCollection实例(不存在返回undefined)
|
||||
*/
|
||||
getPrimitiveById(id: string): Primitive | BillboardCollection | undefined {
|
||||
return this.#defaultPrimitiveMap.get(id) || this.#customPrimitiveMap.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID删除Primitive
|
||||
* @description 自动识别默认/自定义并清理对应存储
|
||||
* @param viewer - Cesium Viewer实例
|
||||
* @param id - Primitive的ID
|
||||
* @returns 是否删除成功
|
||||
*/
|
||||
removePrimitiveById(viewer: Viewer, id: string): boolean {
|
||||
// 先判断Primitive类型
|
||||
const isDefault = this.#defaultPrimitiveMap.has(id)
|
||||
const isCustom = this.#customPrimitiveMap.has(id)
|
||||
|
||||
if (!isDefault && !isCustom) {
|
||||
console.warn(`Primitive ID ${id} 不存在`)
|
||||
return false
|
||||
}
|
||||
|
||||
const primitive = isDefault ? this.#defaultPrimitiveMap.get(id) : this.#customPrimitiveMap.get(id)
|
||||
if (primitive) {
|
||||
// 从场景中移除
|
||||
viewer.scene.primitives.remove(primitive)
|
||||
// 清理对应映射
|
||||
if (isDefault) {
|
||||
this.#defaultPrimitiveMap.delete(id)
|
||||
} else {
|
||||
this.#customPrimitiveMap.delete(id)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除Primitive(支持按类型筛选)
|
||||
* @description 精准清除默认/自定义/全部Primitive
|
||||
* @param viewer - Cesium Viewer实例
|
||||
* @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
|
||||
*/
|
||||
clearAllPrimitives(viewer: Viewer, clearType: ClearType = 'custom'): void {
|
||||
const targetMap = new Map<string, Primitive | BillboardCollection>()
|
||||
|
||||
// 确定要清除的Primitive映射
|
||||
if (clearType === 'default') {
|
||||
this.#defaultPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
|
||||
} else if (clearType === 'custom') {
|
||||
this.#customPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
|
||||
} else {
|
||||
this.#defaultPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
|
||||
this.#customPrimitiveMap.forEach((value, key) => targetMap.set(key, value))
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
targetMap.forEach((primitive) => {
|
||||
viewer.scene.primitives.remove(primitive)
|
||||
})
|
||||
|
||||
// 清理对应映射
|
||||
if (clearType === 'default') {
|
||||
this.#defaultPrimitiveMap.clear()
|
||||
} else if (clearType === 'custom') {
|
||||
this.#customPrimitiveMap.clear()
|
||||
} else {
|
||||
this.#defaultPrimitiveMap.clear()
|
||||
this.#customPrimitiveMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通用图层(支持imagery/wms/wmts)
|
||||
* @description 区分默认/自定义图层,精准管理
|
||||
* @param viewer - Cesium Viewer实例
|
||||
* @param layerConfig - 图层配置(含default标识)
|
||||
* @returns 创建的ImageryLayer实例(失败返回null)
|
||||
*/
|
||||
createLayer(viewer: Viewer, layerConfig: LayerConfig): ImageryLayer | null {
|
||||
if (!layerConfig.layers) {
|
||||
throw new Error('layers 参数未定义')
|
||||
}
|
||||
|
||||
const { layers: layerKey, default: isDefault = false } = layerConfig
|
||||
|
||||
// 校验图层唯一性(跨默认/自定义)
|
||||
if (this.#defaultLayerMap.has(layerKey) || this.#customLayerMap.has(layerKey)) {
|
||||
console.warn(`图层 ${layerKey} 已存在,将覆盖原有图层`)
|
||||
this.removeLayerByKey(layerKey, viewer)
|
||||
}
|
||||
|
||||
let provider: ImageryProvider | null = null
|
||||
switch (layerConfig.type) {
|
||||
case 'imagery':
|
||||
provider = new ArcGisMapServerImageryProvider({ url: layerConfig.url })
|
||||
break
|
||||
case 'wms': // Geoserver WMS
|
||||
provider = new WebMapServiceImageryProvider({
|
||||
url: layerConfig.url,
|
||||
layers: layerConfig.layers,
|
||||
parameters: layerConfig.parameters || { format: 'image/png' },
|
||||
})
|
||||
break
|
||||
case 'wmts': // Geoserver WMTS
|
||||
provider = new WebMapTileServiceImageryProvider({
|
||||
url: layerConfig.url,
|
||||
layer: layerConfig.layers,
|
||||
style: layerConfig.style || 'default',
|
||||
format: layerConfig.format || 'image/png',
|
||||
tileMatrixSetID: layerConfig.tileMatrixSetID || 'EPSG:4326',
|
||||
credit: '',
|
||||
})
|
||||
break
|
||||
default:
|
||||
console.error(`不支持的图层类型:${layerConfig.type}`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (provider) {
|
||||
const layer = viewer.imageryLayers.addImageryProvider(provider)
|
||||
// 按default标识存入对应映射
|
||||
if (isDefault) {
|
||||
this.#defaultLayerMap.set(layerKey, layer)
|
||||
} else {
|
||||
this.#customLayerMap.set(layerKey, layer)
|
||||
}
|
||||
return layer
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图层key(layerConfig.layers)获取图层实例
|
||||
* @description 自动识别默认/自定义图层
|
||||
* @param key - 图层key(layerConfig.layers)
|
||||
* @returns ImageryLayer实例(不存在返回undefined)
|
||||
*/
|
||||
getLayerByKey(key: string): ImageryLayer | undefined {
|
||||
return this.#defaultLayerMap.get(key) || this.#customLayerMap.get(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据图层key删除图层
|
||||
* @description 自动识别默认/自定义并清理对应存储
|
||||
* @param key - 图层key(layerConfig.layers)
|
||||
* @param viewer - Cesium Viewer实例
|
||||
* @returns 是否删除成功
|
||||
*/
|
||||
removeLayerByKey(key: string, viewer: Viewer): boolean {
|
||||
// 先判断图层类型
|
||||
const isDefault = this.#defaultLayerMap.has(key)
|
||||
const isCustom = this.#customLayerMap.has(key)
|
||||
|
||||
if (!isDefault && !isCustom) {
|
||||
console.warn(`图层 key ${key} 不存在`)
|
||||
return false
|
||||
}
|
||||
|
||||
const layer = isDefault ? this.#defaultLayerMap.get(key) : this.#customLayerMap.get(key)
|
||||
if (layer) {
|
||||
// 从场景中移除图层
|
||||
viewer.imageryLayers.remove(layer)
|
||||
// 清理对应映射
|
||||
if (isDefault) {
|
||||
this.#defaultLayerMap.delete(key)
|
||||
} else {
|
||||
this.#customLayerMap.delete(key)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除图层(支持按类型筛选)
|
||||
* @description 精准清除默认/自定义/全部自定义图层
|
||||
* @param viewer - Cesium Viewer实例
|
||||
* @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
|
||||
*/
|
||||
clearAllCustomLayers(viewer: Viewer, clearType: ClearType = 'custom'): void {
|
||||
const targetMap = new Map<string, ImageryLayer>()
|
||||
|
||||
// 确定要清除的图层映射
|
||||
if (clearType === 'default') {
|
||||
this.#defaultLayerMap.forEach((value, key) => targetMap.set(key, value))
|
||||
} else if (clearType === 'custom') {
|
||||
this.#customLayerMap.forEach((value, key) => targetMap.set(key, value))
|
||||
} else {
|
||||
this.#defaultLayerMap.forEach((value, key) => targetMap.set(key, value))
|
||||
this.#customLayerMap.forEach((value, key) => targetMap.set(key, value))
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
targetMap.forEach((layer) => {
|
||||
viewer.imageryLayers.remove(layer)
|
||||
})
|
||||
|
||||
// 清理对应映射
|
||||
if (clearType === 'default') {
|
||||
this.#defaultLayerMap.clear()
|
||||
} else if (clearType === 'custom') {
|
||||
this.#customLayerMap.clear()
|
||||
} else {
|
||||
this.#defaultLayerMap.clear()
|
||||
this.#customLayerMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量清除所有私有资源(实体/Primitive/图层)
|
||||
* @description 支持按类型筛选,精准控制清除范围
|
||||
* @param viewer - Cesium Viewer实例
|
||||
* @param clearType - 清除类型:default(默认)/custom(自定义)/all(全部)
|
||||
*/
|
||||
clearAllPrivateObject(viewer: Viewer, clearType: ClearType = 'custom'): void {
|
||||
this.clearAllEntities(viewer, clearType)
|
||||
this.clearAllPrimitives(viewer, clearType)
|
||||
this.clearAllCustomLayers(viewer, clearType)
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁 Cesium 实例
|
||||
* @description 释放所有内存,避免泄漏(页面卸载时调用)
|
||||
* @param viewer - Cesium Viewer 实例
|
||||
*/
|
||||
destroyCesiumViewer(viewer: Viewer): void {
|
||||
if (viewer) {
|
||||
// 清除所有资源(默认+自定义)
|
||||
this.clearAllPrivateObject(viewer, 'all')
|
||||
// 销毁viewer
|
||||
viewer.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 坐标转换辅助函数
|
||||
*/
|
||||
convertPosition(pos: Cartesian3 | [number, number, number]): Cartesian3 {
|
||||
return Array.isArray(pos) ? Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0) : pos
|
||||
}
|
||||
|
||||
/**
|
||||
* 位置数组转换辅助函数
|
||||
*/
|
||||
convertPositionArray(positions: (Cartesian3 | [number, number, number])[]): Cartesian3[] {
|
||||
return positions.map((pos) => this.convertPosition(pos))
|
||||
}
|
||||
|
||||
/**
|
||||
* 多边形层级处理辅助函数
|
||||
*/
|
||||
#processHierarchy(
|
||||
hier: PolygonHierarchy | Cartesian3[] | [number, number][] | [number, number, number][],
|
||||
): PolygonHierarchy {
|
||||
if (hier instanceof PolygonHierarchy) {
|
||||
return hier
|
||||
}
|
||||
|
||||
if (!Array.isArray(hier) || hier.length < 3) {
|
||||
throw new Error('多边形层级必须是非空数组且至少 3 个顶点')
|
||||
}
|
||||
|
||||
const positions = hier.map((pos) => {
|
||||
if (pos instanceof Cartesian3) {
|
||||
return pos
|
||||
}
|
||||
if (Array.isArray(pos) && pos.length >= 2) {
|
||||
return Cartesian3.fromDegrees(pos[0], pos[1], pos[2] || 0)
|
||||
}
|
||||
throw new Error(
|
||||
`无效坐标格式:${JSON.stringify(pos)},应为 [经, 纬] 或 [经, 纬, 高] 或 Cartesian3`,
|
||||
)
|
||||
})
|
||||
|
||||
return new PolygonHierarchy(positions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建ConstantProperty包装器
|
||||
*/
|
||||
#createConstantProperty(value: unknown): ConstantProperty {
|
||||
return new ConstantProperty(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import axios, { type InternalAxiosRequestConfig, type AxiosResponse } from 'axios'
|
||||
import configJson from '@/config/config.json'
|
||||
import { SafetyUtils } from '../safety/SafetyUtils.ts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from '@/router'
|
||||
|
||||
// 扩展Axios内部配置类型
|
||||
declare module 'axios' {
|
||||
interface InternalAxiosRequestConfig {
|
||||
__sm4Key?: string
|
||||
isNoEncryptUrl?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const httpInstance = axios.create({
|
||||
baseURL: configJson.apiBaseUrl,
|
||||
timeout: 15000, // 增加超时时间
|
||||
withCredentials: true,
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
httpInstance.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig): Promise<InternalAxiosRequestConfig> => {
|
||||
const { url, method } = config
|
||||
|
||||
// 初始化headers
|
||||
config.headers = config.headers || {}
|
||||
|
||||
// 加密处理标记
|
||||
const isNoEncryptUrl = configJson.noEncryptUrls.some((path) => url?.includes(path))
|
||||
config.isNoEncryptUrl = isNoEncryptUrl
|
||||
|
||||
if (!isNoEncryptUrl) {
|
||||
try {
|
||||
// 生成SM4密钥并加密,无论是否有业务参数
|
||||
const sm4Key = SafetyUtils.generateSm4Key()
|
||||
config.__sm4Key = sm4Key
|
||||
const sm4KeyEncrypted = await SafetyUtils.sm2Encrypt(sm4Key)
|
||||
|
||||
// GET请求:处理URL参数(无论是否有params,都要传递sm4KeyEncrypted)
|
||||
if (method?.toUpperCase() === 'GET') {
|
||||
// 有业务参数则加密,无参数则仅传递sm4KeyEncrypted
|
||||
const encryptedParams = config.params ? SafetyUtils.sm4Encrypt(sm4Key, config.params) : '' // 无参数时encryptedData可为空
|
||||
|
||||
config.params = {
|
||||
encryptedData: encryptedParams,
|
||||
sm4KeyEncrypted: sm4KeyEncrypted,
|
||||
}
|
||||
}
|
||||
|
||||
// POST/PUT/DELETE/PATCH请求:处理请求体(无论是否有data,都要传递sm4KeyEncrypted)
|
||||
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method?.toUpperCase() || '')) {
|
||||
if (config.data instanceof FormData) {
|
||||
// 加密表单数据
|
||||
const encryptedFormData = SafetyUtils.encryptFormData(sm4Key, config.data)
|
||||
|
||||
const newFormData = new FormData()
|
||||
// 复制加密后的业务字段
|
||||
for (const [key, value] of encryptedFormData.entries()) {
|
||||
newFormData.append(key, value)
|
||||
}
|
||||
// 强制添加sm4KeyEncrypted
|
||||
newFormData.append('sm4KeyEncrypted', sm4KeyEncrypted)
|
||||
config.data = newFormData
|
||||
} else {
|
||||
// 有业务数据则加密,无数据则encryptedData为空
|
||||
const encryptedData = config.data ? SafetyUtils.sm4Encrypt(sm4Key, config.data) : ''
|
||||
|
||||
config.data = {
|
||||
encryptedData: encryptedData,
|
||||
sm4KeyEncrypted: sm4KeyEncrypted,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求加密失败:', error)
|
||||
return Promise.reject(new Error('请求数据加密失败'))
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
console.error('请求拦截器错误:', error)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
httpInstance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
const { config, data } = response
|
||||
let processedData // 用于存储处理后(原始或解密)的数据
|
||||
|
||||
// 处理非加密接口或无密钥的情况
|
||||
if (config.isNoEncryptUrl || !config.__sm4Key) {
|
||||
processedData = data
|
||||
} else {
|
||||
// 处理加密接口的解密逻辑
|
||||
try {
|
||||
if (typeof data === 'string') {
|
||||
// 解密字符串类型的加密数据
|
||||
processedData = SafetyUtils.sm4Decrypt(config.__sm4Key, data)
|
||||
} else if (data && typeof data === 'object') {
|
||||
// 解密对象中包含的加密字段
|
||||
processedData = SafetyUtils.sm4Decrypt(config.__sm4Key, data.encryptedData || data)
|
||||
} else {
|
||||
// 非预期数据格式直接使用原始数据
|
||||
processedData = data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('响应数据解密失败:', error)
|
||||
ElMessage.error('数据解密失败,请重试')
|
||||
return Promise.reject(new Error('数据解密失败,请重试'))
|
||||
}
|
||||
}
|
||||
|
||||
// 统一判断处理后的数据状态
|
||||
if (processedData?.code === 200 || processedData?.code === 409) {
|
||||
return processedData
|
||||
} else if (processedData?.code == 401) {
|
||||
router.push(`/login?redirect=${router.currentRoute.value.fullPath}`)
|
||||
ElMessage.error('请先登录')
|
||||
} else {
|
||||
const errorMsg = processedData?.message || '操作失败,请稍后重试'
|
||||
ElMessage.error(errorMsg)
|
||||
return Promise.reject(new Error(errorMsg))
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('响应拦截器错误:', error)
|
||||
let errorMsg = '请求失败,请稍后重试'
|
||||
|
||||
// 处理服务器解密相关错误
|
||||
if (error.response?.status === 500 && error.response?.data?.msg?.includes('解密')) {
|
||||
errorMsg = '服务器解密失败,请检查密钥配置'
|
||||
} else if (error.message) {
|
||||
// 使用错误对象自带的消息
|
||||
errorMsg = error.message
|
||||
}
|
||||
|
||||
// 错误提示
|
||||
ElMessage.error(errorMsg)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export default httpInstance
|
||||
@@ -0,0 +1,146 @@
|
||||
import { $api } from '@/api/api'
|
||||
import { useCryptStore } from '@/stores/useCryptStore'
|
||||
import { SM2, SM4 } from 'gm-crypto'
|
||||
|
||||
// 与后端SM2Utils对应的模式配置
|
||||
const SM2_MODE = SM2.constants.C1C3C2
|
||||
const SM2_INPUT_ENCODING = 'utf8'
|
||||
const SM2_OUTPUT_ENCODING = 'hex'
|
||||
|
||||
// 与后端SM4Utils对应的模式配置(ECB模式)
|
||||
const SM4_MODE = SM4.constants.ECB
|
||||
const SM4_INPUT_ENCODING = 'utf8'
|
||||
const SM4_OUTPUT_ENCODING = 'hex'
|
||||
|
||||
export const SafetyUtils = {
|
||||
/**
|
||||
* 获取SM2公钥
|
||||
*/
|
||||
getSm2PublicKey: async (): Promise<string> => {
|
||||
const cryptStore = useCryptStore()
|
||||
if (cryptStore.sm2PublicKey) return cryptStore.sm2PublicKey
|
||||
|
||||
try {
|
||||
const res = await $api.crypto.getSm2PublicKey()
|
||||
const publicKey = res.data.publicKey
|
||||
cryptStore.sm2PublicKey = publicKey
|
||||
return publicKey
|
||||
} catch (error) {
|
||||
console.error('获取SM2公钥失败:', error)
|
||||
throw new Error('获取加密公钥失败')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成随机SM4密钥(16字节=32位十六进制字符串)
|
||||
*/
|
||||
generateSm4Key: (): string => {
|
||||
return SafetyUtils._generateRandomHex(16)
|
||||
},
|
||||
|
||||
/**
|
||||
* SM2非对称加密(公钥加密)
|
||||
*/
|
||||
sm2Encrypt: async (data: object | string, publicKey?: string): Promise<string> => {
|
||||
try {
|
||||
const targetPublicKey = publicKey || (await SafetyUtils.getSm2PublicKey())
|
||||
const plaintext = typeof data === 'string' ? data : JSON.stringify(data)
|
||||
return (
|
||||
'04' +
|
||||
SM2.encrypt(plaintext, targetPublicKey, {
|
||||
mode: SM2_MODE,
|
||||
inputEncoding: SM2_INPUT_ENCODING,
|
||||
outputEncoding: SM2_OUTPUT_ENCODING,
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('SM2加密失败:', error)
|
||||
throw new Error('数据加密失败')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* SM2非对称解密(私钥解密)
|
||||
*/
|
||||
sm2Decrypt: async (data: string, privateKey: string): Promise<string> => {
|
||||
try {
|
||||
return SM2.decrypt(data.substring(2), privateKey, {
|
||||
inputEncoding: SM2_OUTPUT_ENCODING,
|
||||
outputEncoding: SM2_INPUT_ENCODING,
|
||||
mode: SM2.constants.C1C3C2,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('SM2加密失败:', error)
|
||||
throw new Error('数据加密失败')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* SM4对称加密(ECB模式)
|
||||
*/
|
||||
sm4Encrypt: (key: string, data: object | string): string => {
|
||||
try {
|
||||
const plaintext = typeof data === 'string' ? data : JSON.stringify(data)
|
||||
return SM4.encrypt(plaintext, key, {
|
||||
mode: SM4_MODE,
|
||||
inputEncoding: SM4_INPUT_ENCODING,
|
||||
outputEncoding: SM4_OUTPUT_ENCODING,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('SM4加密失败:', error)
|
||||
throw new Error('数据加密失败')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* SM4对称解密(ECB模式)
|
||||
*/
|
||||
sm4Decrypt: (key: string, encryptedData: string): object | string => {
|
||||
try {
|
||||
const plaintext = SM4.decrypt(encryptedData, key, {
|
||||
mode: SM4_MODE,
|
||||
inputEncoding: SM4_OUTPUT_ENCODING,
|
||||
outputEncoding: SM4_INPUT_ENCODING,
|
||||
})
|
||||
|
||||
// 尝试解析JSON,兼容对象和字符串
|
||||
try {
|
||||
return JSON.parse(plaintext)
|
||||
} catch {
|
||||
return plaintext
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SM4解密失败:', error)
|
||||
throw new Error('数据解密失败')
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 加密FormData中的普通字段
|
||||
*/
|
||||
encryptFormData: (key: string, formData: FormData): FormData => {
|
||||
const encryptedFormData = new FormData()
|
||||
|
||||
for (const [fieldName, value] of formData.entries()) {
|
||||
if (value instanceof Blob) {
|
||||
// 保留文件字段
|
||||
encryptedFormData.append(fieldName, value, (value as File).name)
|
||||
} else {
|
||||
// 加密普通字段
|
||||
const encryptedValue = SafetyUtils.sm4Encrypt(key, String(value))
|
||||
encryptedFormData.append(fieldName, encryptedValue)
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedFormData
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成指定长度的随机十六进制字符串
|
||||
*/
|
||||
_generateRandomHex: (length: number): string => {
|
||||
const uint8Array = new Uint8Array(length)
|
||||
window.crypto.getRandomValues(uint8Array)
|
||||
return Array.from(uint8Array, (byte) => byte.toString(16).padStart(2, '0')).join('')
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* 工具类集合
|
||||
* 包含防抖等通用工具函数(无显式any类型)
|
||||
*/
|
||||
export const Utils = {
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param func - 需要防抖的函数
|
||||
* @param delay - 延迟时间(毫秒),默认500ms
|
||||
* @param immediate - 是否立即执行,默认false
|
||||
* @returns 防抖处理后的函数
|
||||
*/
|
||||
debounce: function <This, T extends unknown[], R = void>(
|
||||
func: (this: This, ...args: T) => R,
|
||||
delay: number = 500,
|
||||
immediate: boolean = false,
|
||||
) {
|
||||
let timer: number | null = null
|
||||
|
||||
// 用泛型This指定this类型
|
||||
return function (this: This, ...args: T): void {
|
||||
if (timer) clearTimeout(timer)
|
||||
|
||||
// 立即执行逻辑
|
||||
if (immediate && !timer) {
|
||||
func.apply(this, args)
|
||||
}
|
||||
|
||||
// 重新设置定时器
|
||||
timer = window.setTimeout(() => {
|
||||
if (!immediate) {
|
||||
func.apply(this, args)
|
||||
}
|
||||
timer = null
|
||||
}, delay)
|
||||
}
|
||||
},
|
||||
|
||||
formatDate: (format: string, date: Date = new Date()): string => {
|
||||
// 基础时间数据
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1 // 月份0-11,需+1
|
||||
const day = date.getDate()
|
||||
const hours24 = date.getHours()
|
||||
const hours12 = hours24 % 12 || 12 // 12小时制处理(0→12)
|
||||
const minutes = date.getMinutes()
|
||||
const seconds = date.getSeconds()
|
||||
const weekNum = date.getDay() // 星期0-6(0=周日)
|
||||
|
||||
// 星期映射配置
|
||||
const weekMaps = {
|
||||
ddd: ['日', '一', '二', '三', '四', '五', '六'].map((day) => `星期${day}`),
|
||||
dd: ['日', '一', '二', '三', '四', '五', '六'].map((day) => `周${day}`),
|
||||
d: [0, 1, 2, 3, 4, 5, 6],
|
||||
}
|
||||
|
||||
// 占位符替换规则(顺序:长占位符优先,避免冲突)
|
||||
const replaceRules = [
|
||||
{ regex: /YYYY/g, value: year.toString() },
|
||||
{ regex: /YY/g, value: year.toString().slice(-2) },
|
||||
{ regex: /MM/g, value: month.toString().padStart(2, '0') },
|
||||
{ regex: /M/g, value: month.toString() },
|
||||
{ regex: /DD/g, value: day.toString().padStart(2, '0') },
|
||||
{ regex: /D/g, value: day.toString() },
|
||||
{ regex: /HH/g, value: hours24.toString().padStart(2, '0') },
|
||||
{ regex: /H/g, value: hours24.toString() },
|
||||
{ regex: /hh/g, value: hours12.toString().padStart(2, '0') },
|
||||
{ regex: /h/g, value: hours12.toString() },
|
||||
{ regex: /mm/g, value: minutes.toString().padStart(2, '0') },
|
||||
{ regex: /m/g, value: minutes.toString() },
|
||||
{ regex: /ss/g, value: seconds.toString().padStart(2, '0') },
|
||||
{ regex: /s/g, value: seconds.toString() },
|
||||
{ regex: /ddd/g, value: weekMaps.ddd[weekNum] },
|
||||
{ regex: /dd/g, value: weekMaps.dd[weekNum] },
|
||||
{ regex: /d/g, value: weekMaps.d[weekNum] },
|
||||
]
|
||||
|
||||
// 执行替换
|
||||
return replaceRules.reduce((result, { regex, value }) => {
|
||||
return result.replace(regex, String(value ?? ''))
|
||||
}, format)
|
||||
},
|
||||
/**
|
||||
* 深拷贝函数
|
||||
* 支持类型:原始类型、数组、对象、Date、RegExp、Map、Set、ArrayBuffer等
|
||||
* @param source 要拷贝的数据源
|
||||
* @param hash 用于解决循环引用的哈希表,内部使用
|
||||
* @returns 深拷贝后的新数据
|
||||
*/
|
||||
deepClone: <T>(source: T, hash = new WeakMap<object, unknown>()): T => {
|
||||
// 处理 null 或 undefined
|
||||
if (source === null || source === undefined) {
|
||||
return source
|
||||
}
|
||||
|
||||
// 处理原始类型(string, number, boolean, symbol, bigint, function)
|
||||
if (typeof source !== 'object' && typeof source !== 'function') {
|
||||
return source
|
||||
}
|
||||
|
||||
// 处理函数 - 直接返回原函数引用(通常不需要克隆函数)
|
||||
if (typeof source === 'function') {
|
||||
return source
|
||||
}
|
||||
|
||||
// 解决循环引用
|
||||
if (hash.has(source)) {
|
||||
return hash.get(source) as T
|
||||
}
|
||||
|
||||
// 处理 Date 对象
|
||||
if (source instanceof Date) {
|
||||
const cloned = new Date(source.getTime()) as T
|
||||
hash.set(source, cloned)
|
||||
return cloned
|
||||
}
|
||||
|
||||
// 处理 RegExp 对象
|
||||
if (source instanceof RegExp) {
|
||||
const cloned = new RegExp(source.source, source.flags)
|
||||
cloned.lastIndex = source.lastIndex // 安全访问
|
||||
return cloned as unknown as T
|
||||
}
|
||||
// 处理 Map 对象
|
||||
if (source instanceof Map) {
|
||||
const cloned = new Map()
|
||||
hash.set(source, cloned)
|
||||
source.forEach((value, key) => {
|
||||
cloned.set(Utils.deepClone(key, hash), Utils.deepClone(value, hash))
|
||||
})
|
||||
return cloned as T
|
||||
}
|
||||
|
||||
// 处理 Set 对象
|
||||
if (source instanceof Set) {
|
||||
const cloned = new Set<unknown>()
|
||||
hash.set(source, cloned)
|
||||
for (const value of source.values()) {
|
||||
cloned.add(Utils.deepClone(value, hash))
|
||||
}
|
||||
return cloned as T
|
||||
}
|
||||
|
||||
// 处理 ArrayBuffer
|
||||
if (source instanceof ArrayBuffer) {
|
||||
const cloned = source.slice(0) as T
|
||||
hash.set(source, cloned)
|
||||
return cloned
|
||||
}
|
||||
|
||||
// 处理数组
|
||||
if (Array.isArray(source)) {
|
||||
const cloned: T[] = []
|
||||
hash.set(source, cloned)
|
||||
for (let i = 0; i < source.length; i++) {
|
||||
cloned[i] = Utils.deepClone(source[i], hash)
|
||||
}
|
||||
return cloned as T
|
||||
}
|
||||
|
||||
// 处理普通对象
|
||||
if (typeof source === 'object') {
|
||||
// 处理 Error 对象
|
||||
if (source instanceof Error) {
|
||||
const cloned = new Error(source.message)
|
||||
cloned.stack = source.stack
|
||||
cloned.name = source.name
|
||||
hash.set(source, cloned)
|
||||
return cloned as T
|
||||
}
|
||||
|
||||
// 处理其他对象
|
||||
const cloned: { [key: string | symbol]: unknown } = {}
|
||||
hash.set(source, cloned)
|
||||
|
||||
// 获取对象的所有属性(包括不可枚举的属性和 Symbol)
|
||||
const keys = [...Object.getOwnPropertyNames(source), ...Object.getOwnPropertySymbols(source)]
|
||||
|
||||
for (const key of keys) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(source, key)
|
||||
|
||||
// 如果是访问器属性
|
||||
if (descriptor && descriptor.get) {
|
||||
Object.defineProperty(cloned, key, descriptor)
|
||||
} else {
|
||||
// 如果是数据属性
|
||||
cloned[key] = Utils.deepClone((source as { [key: string | symbol]: unknown })[key], hash)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理原型链
|
||||
const proto = Object.getPrototypeOf(source)
|
||||
if (proto && proto !== Object.prototype) {
|
||||
Object.setPrototypeOf(cloned, proto)
|
||||
}
|
||||
|
||||
return cloned as T
|
||||
}
|
||||
|
||||
// 对于其他无法处理的情况,返回原值
|
||||
return source
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
首页
|
||||
</div>
|
||||
</template>
|
||||
<script name="HomePage" setup lang="ts">
|
||||
</script>
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import config from './src/config/config.json'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import cesium from 'vite-plugin-cesium'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
cesium(),
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: config.backendBaseUrl,
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user