Initial commit

This commit is contained in:
wzy-warehouse
2026-04-07 20:01:06 +08:00
committed by GitHub
commit b87a0e71db
34 changed files with 6035 additions and 0 deletions
+24
View File
@@ -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?
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+21
View File
@@ -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.
+68
View File
@@ -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文件。
+10
View File
@@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}
+17
View File
@@ -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']
}
}
+22
View File
@@ -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
View File
@@ -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>
+43
View File
@@ -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"
}
}
+4081
View File
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
View File
@@ -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>
+10
View File
@@ -0,0 +1,10 @@
import { getSm2PublicKey } from './crypto'
export const $api = {
// 加密模块
crypto: {
// 获取sm2公钥
getSm2PublicKey: () => getSm2PublicKey(),
},
}
+11
View File
@@ -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')
}
+1
View File
@@ -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

+12
View File
@@ -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
View File
@@ -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')
+18
View File
@@ -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
+9
View File
@@ -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 }
})
+5
View File
@@ -0,0 +1,5 @@
export interface Response<T = unknown> {
code: number
message: string
data: T
}
+22
View File
@@ -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
}
+46
View File
@@ -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> // 自定义属性(用于存储额外信息)
}
+21
View File
@@ -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> // 图层参数
}
+13
View File
@@ -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 // 广告牌缩放
}
+6
View File
@@ -0,0 +1,6 @@
export interface Sm2PublicKeyResponse {
/**
* 公钥
*/
publicKey: string
}
+935
View File
@@ -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: IDvalue: 实例)
#defaultPrimitiveMap: Map<string, Primitive | BillboardCollection> = new Map<
string,
Primitive | BillboardCollection
>()
// 私有属性:自定义Primitive映射
#customPrimitiveMap: Map<string, Primitive | BillboardCollection> = new Map<
string,
Primitive | BillboardCollection
>()
// ===================== 图层管理(区分默认/自定义) =====================
// 私有属性:默认图层映射(key: layerConfig.layersvalue: 实例)
#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、typedefault标识是否为默认实体)
* @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
}
/**
* 根据图层keylayerConfig.layers)获取图层实例
* @description 自动识别默认/自定义图层
* @param key - 图层keylayerConfig.layers
* @returns ImageryLayer实例(不存在返回undefined
*/
getLayerByKey(key: string): ImageryLayer | undefined {
return this.#defaultLayerMap.get(key) || this.#customLayerMap.get(key)
}
/**
* 根据图层key删除图层
* @description 自动识别默认/自定义并清理对应存储
* @param key - 图层keylayerConfig.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)
}
}
+148
View File
@@ -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
+146
View File
@@ -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('')
},
}
+203
View File
@@ -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-60=周日)
// 星期映射配置
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
},
}
+9
View File
@@ -0,0 +1,9 @@
<template>
<div>
首页
</div>
</template>
<script name="HomePage" setup lang="ts">
</script>
<style scoped>
</style>
+14
View File
@@ -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"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+24
View File
@@ -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"]
}
+39
View File
@@ -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/, ''),
},
},
},
})