点线标注

This commit is contained in:
zh
2025-09-04 21:24:42 +08:00
parent e31d7c7ae0
commit 85121ac6a8
70 changed files with 629 additions and 174 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules node_modules
dist dist
build
out out
.history .history
.DS_Store .DS_Store

View File

@ -1,6 +1,6 @@
{ {
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"

6
package-lock.json generated
View File

@ -15,6 +15,7 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"element-plus": "^2.10.4", "element-plus": "^2.10.4",
"js-md5": "^0.8.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.4.1", "pinia-plugin-persistedstate": "^4.4.1",
@ -8452,6 +8453,11 @@
"dev": true, "dev": true,
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/js-md5": {
"version": "0.8.3",
"resolved": "https://registry.npmmirror.com/js-md5/-/js-md5-0.8.3.tgz",
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ=="
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -5,8 +5,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Electron</title> <title>Electron</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src 'self' http://127.0.0.1:8808; script-src 'self' 'unsafe-eval' 'unsafe-inline'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:;" />
<style src="/sdk/custom/css/index.css"></style> <style src="/sdk/custom/css/index.css"></style>
</head> </head>

File diff suppressed because one or more lines are too long

View File

@ -2212,6 +2212,34 @@
.YJ-custom-base-dialog.cut-fill>.content>div .div-item:last-child .row .unit { .YJ-custom-base-dialog.cut-fill>.content>div .div-item:last-child .row .unit {
margin-left: 5px; margin-left: 5px;
} }
.YJ-custom-base-dialog.cut-fill>.content>div .el-slider__button {
width: 16px;
height: 16px;
border: 2px solid rgba(var(--color-sdk-base-rgb), 1);
}
.YJ-custom-base-dialog.cut-fill>.content>div .el-slider__bar {
background-color: rgba(var(--color-sdk-base-rgb), 1);
}
.YJ-custom-base-dialog.cut-fill>.content>div .firstTip {
font-size: 14px;
font-weight: 700;
letter-spacing: 0px;
line-height: 0px;
color: rgba(255, 255, 255, 1);
position: absolute;
top: 157px;
left: 340px;
}
.YJ-custom-base-dialog.cut-fill>.content>div .endTip {
font-size: 14px;
font-weight: 700;
letter-spacing: 0px;
line-height: 0px;
color: rgba(255, 255, 255, 1);
position: absolute;
top: 157px;
left: 515px;
}
/* 淹没分析 */ /* 淹没分析 */
.YJ-custom-base-dialog.submerge>.content>div .row>.col { .YJ-custom-base-dialog.submerge>.content>div .row>.col {
@ -2415,6 +2443,41 @@
.YJ-custom-base-dialog.circle-view-shed>.content { .YJ-custom-base-dialog.circle-view-shed>.content {
width: 290px; width: 290px;
} }
.YJ-custom-base-dialog.circle-view-shed>.content>div .el-slider__button {
width: 16px;
height: 16px;
border: 2px solid rgba(var(--color-sdk-base-rgb), 1);
}
.YJ-custom-base-dialog.circle-view-shed>.content>div .el-slider__bar {
background-color: rgba(var(--color-sdk-base-rgb), 1);
}
.YJ-custom-base-dialog.circle-view-shed>.content>div .firstTip {
font-size: 14px;
font-weight: 700;
letter-spacing: 0px;
line-height: 0px;
color: rgba(255, 255, 255, 1);
position: absolute;
top: 150px;
left: 88px;
}
.YJ-custom-base-dialog.circle-view-shed>.content>div .endTip {
font-size: 14px;
font-weight: 700;
letter-spacing: 0px;
line-height: 0px;
color: rgba(255, 255, 255, 1);
position: absolute;
top: 150px;
left: 240px;
}
.el-popper.is-dark {
z-index: 100000000 !important;
/* 确保这个值足够高 */
}
/* 地形可视域分析 */ /* 地形可视域分析 */
.YJ-custom-base-dialog.visibility>.content { .YJ-custom-base-dialog.visibility>.content {

View File

@ -64,6 +64,71 @@ export default {
ersanwei: '二三维', ersanwei: '二三维',
junbiao3d: '三维军标' junbiao3d: '三维军标'
}, },
effect: {
trajectoryMotion:"轨迹运动",
electronicFence: "电子围墙",
// nightVision: '实体墙',
radarLightWave: "扩散光波",
diffusedLightWave: "雷达光波",
scanStereoscopic: "立体雷达",
multilateralBody: "多边体",
waterSurface: "水面",
fountain: '喷泉',
waterL: '水柱',
fire: "火焰",
explosion: "爆炸",
smoke: "烟雾",
nightVision: '夜视',
// nightVision: '飞线',
},
analysis:{
inundationAnalysis: "淹没分析",
profileAnalysis: "剖面分析",
sightAnalysis: "视线分析",
kenAnalysis: "视域分析",
circleKen: "圆形视域",
slopeDirection: "坡度坡向",
cutFill: "土方分析",
globalContour: "全局等高线",
contour: "等高线",
clear: "清除",
},
measure:{
projectionArea: "投影面积",
projectionDistanceMeasure: '投影距离',
areaMeasure: "贴地面积",
distanceMeasure: "贴地距离",
heightMeasure: "垂直高度",
triangleMeasure: "空间三角",
MeasureAzimuth: '方位角',
MeasureAngle: "夹角",
lopeDistanceMeasures: '坡度',
coorMeasure: "坐标",
clear: "清除测量",
},
tool:{
routePlan: "路径规划",
//清除轨迹
graffiti: "涂鸦",
// stopGraffiti: "结束涂鸦",
clearGraffiti: "清除涂鸦",
path: "飞行漫游",
coorLocation: "坐标定位",
mouseLocation: "鼠标定位",
annotationAggregation: "标注点聚合",
// 卷帘对比
// 屏幕截图
// 高清出图
// 视频录制
pressModel: "模型压平",
terrainDig: "地形开挖",
tilesetClipping: "剖切",
clearTilesetClipping: "清除剖切",
projConvert: '度分秒',
projectionConvert: '投影转换',
gdbImport: "gdb导入"
},
bottomMenu: { bottomMenu: {
groundText: '贴地文字', groundText: '贴地文字',
standText: '立体文字', standText: '立体文字',

View File

@ -7,5 +7,11 @@ export const LoginApi = {
url: `/user/login`, url: `/user/login`,
data data
}) })
},
logout: async () => {
return await request.post({
url: `/user/logout`,
})
} }
} }

View File

@ -7,6 +7,14 @@ export const TreeApi = {
url: `/source/list` url: `/source/list`
}) })
}, },
// 新增其他资源 /source/addOtherSource
addOtherSource: async (data: any) => {
return await request.post({
url: `/source/addOtherSource`,
data
})
},
//新增节点 //新增节点
addDirectory: async (data: any) => { addDirectory: async (data: any) => {
return await request.post({ return await request.post({

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 788 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 712 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,3 +1,4 @@
import router from '@renderer/router'
import axios from 'axios' import axios from 'axios'
import type { import type {
AxiosInstance, AxiosInstance,
@ -9,14 +10,14 @@ import type {
const pendingRequests = new Map<string, AbortController>() const pendingRequests = new Map<string, AbortController>()
let baseURL: any let baseURL: any
if (window && window.process && window.process.type === 'renderer') { if (window && window.process && window.process.type === 'renderer') {
baseURL = localStorage.getItem('ip') || 'http://127.0.0.1:8808' baseURL = localStorage.getItem('ip') ||'http://192.168.110.25:8848'|| 'http://127.0.0.1:8808'
} else { } else {
baseURL = '' baseURL = ''
} }
// 创建自定义配置的axios实例 // 创建自定义配置的axios实例
const service: AxiosInstance = axios.create({ const service: AxiosInstance = axios.create({
baseURL, baseURL:'http://192.168.110.25:8848',
timeout: 10000, timeout: 10000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -47,9 +48,12 @@ service.interceptors.request.use(
pendingRequests.set(key, controller) pendingRequests.set(key, controller)
// 在这里添加认证token // 在这里添加认证token
const token = localStorage.getItem('access_token') const token = localStorage.getItem('Authorization')
console.log("localStorage.getItem('Authorization')",token);
if (token && config.headers) { if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}` // Bearer
config.headers.Authorization = `${token}`
} }
return config return config
}, },
@ -63,18 +67,24 @@ service.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
const key = getRequestKey(response.config) const key = getRequestKey(response.config)
pendingRequests.delete(key) pendingRequests.delete(key)
console.log(response);
// 统一处理HTTP状态码 // 统一处理HTTP状态码
if (response.status === 200) { if (response.status === 200) {
if (response.data.code == 0) { if ([0,200].includes(response.data.code)) {
return response return response
} }
if (response.data.code != 0) { if (response.data.code==401) {
router.push('/')
localStorage.removeItem('Authorization')
}
if (![0,200].includes(response.data.code)) {
ElMessage({ ElMessage({
message: response.data.msg || response.data.message, message: response.data.msg || response.data.message,
type: 'error' type: 'error'
}) })
} }
} }
return Promise.reject(new Error('Error')) return Promise.reject(new Error('Error'))
}, },

View File

@ -25,7 +25,7 @@ import '../public/tree/jquery.ztree.exhide.js'
import '../public/tree/fuzzysearch.js' import '../public/tree/fuzzysearch.js'
import '../public/tree/newFuzzySearch' import '../public/tree/newFuzzySearch'
import Pagination from './components/Pagination/index.vue' import Pagination from './components/Pagination/index.vue'
process.env["ELECTRON_DISABLE_SECURITY_WARNINGS"] = "true";
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
locale: 'zh-CN', locale: 'zh-CN',
@ -38,6 +38,9 @@ const i18n = createI18n({
// 注册全局指令 // 注册全局指令
const setApp = createApp(App) const setApp = createApp(App)
// 定义全局方法
// setApp.config.globalProperties.$md5 = md5
setApp.component('Pagination', Pagination) setApp.component('Pagination', Pagination)
setupStore(setApp) setupStore(setApp)
setupSvgIcon(setApp) setupSvgIcon(setApp)

View File

@ -23,5 +23,16 @@ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes routes
}) })
router.beforeEach((to, from, next) => {
// 去登录,放行
if (to.path === '/') {
next()
} else {
if(localStorage.getItem('Authorization')) {
next()
}else
next("/")
}
})
export default router export default router

View File

@ -13,14 +13,8 @@
</div> </div>
</div> </div>
</div> --> </div> -->
<div <div class="set_item" :title="t('iconTitle.' + item.name)" v-for="(item, i) of setList" :key="item.id"
class="set_item" :class="{ 'last-item': i === setList.length - 1 }" @click="item.callback">
:title="t('iconTitle.' + item.name)"
v-for="(item, i) of setList"
:key="item.id"
:class="{ 'last-item': i === setList.length - 1 }"
@click="item.callback"
>
<svg-icon :name="item.icon" :size="20"></svg-icon> <svg-icon :name="item.icon" :size="20"></svg-icon>
</div> </div>
<setPup ref="setPupRef"></setPup> <setPup ref="setPupRef"></setPup>
@ -28,14 +22,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import setPup from '../setPup/setPup.vue' import setPup from '../setPup/setPup.vue'
const router = useRouter() // 路由实例
const { t } = useI18n() const { t } = useI18n()
import { uesSetTool } from './hooks' import { uesSetTool } from './hooks'
import { LoginApi } from '@renderer/api/login'
const setPupRef = ref() const setPupRef = ref()
const { setShow } = uesSetTool(setPupRef) const { setShow } = uesSetTool(setPupRef)
const logout = async () => {
let res = await LoginApi.logout()
console.log(res);
if (res.code === 200) {
router.push({ path: '/' })
localStorage.clear()
}
}
const setList = ref([ const setList = ref([
// 标准版本 // 标准版本
{ {
@ -81,8 +84,8 @@ const setList = ref([
icon: 'out_login', icon: 'out_login',
name: 'logout', name: 'logout',
className: 'header_public', className: 'header_public',
dbcallback: null dbcallback: null,
// callback: this.logout, callback: logout,
} }
]) ])
</script> </script>
@ -110,6 +113,7 @@ const setList = ref([
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
.set_item::after { .set_item::after {
content: ''; content: '';
position: absolute; position: absolute;
@ -117,17 +121,16 @@ const setList = ref([
top: 0; top: 0;
bottom: 0; bottom: 0;
width: 1px; width: 1px;
background: linear-gradient( background: linear-gradient(180deg,
180deg, rgba(0, 255, 255, 0),
rgba(0, 255, 255, 0), rgba(0, 255, 255, 1),
rgba(0, 255, 255, 1), rgba(204, 204, 204, 0));
rgba(204, 204, 204, 0)
);
} }
.set_item.last-item::after { .set_item.last-item::after {
display: none; display: none;
} }
::v-deep .el-dialog__body { ::v-deep .el-dialog__body {
padding: 10px; padding: 10px;
} }

View File

@ -2,19 +2,19 @@
<div class="leftBox"> <div class="leftBox">
<div class="left animate__animated"> <div class="left animate__animated">
<div class="menus"> <div class="menus">
<div <div class="menus_itemBox" v-for="(item, index) in menuList" :title="t(`firstMenu.${item.name}`)">
class="menus_itemBox" <div class="item_icon" @click="(e) => { handleClick(item, e) }">
v-for="(item, index) in menuList"
:title="t(`firstMenu.${item.name}`)"
>
<div class="item_icon">
<!-- <svg-icon :class-name="['absolute', 'zIndex-1', 'left_item_bg']" icon-class="bg2"></svg-icon> --> <!-- <svg-icon :class-name="['absolute', 'zIndex-1', 'left_item_bg']" icon-class="bg2"></svg-icon> -->
<svg-icon :name="item.svg" :size="16" color="rgba(0, 255, 255, 1)"></svg-icon> <svg-icon :name="item.svg" :size="16" color="rgba(0, 255, 255, 1)"></svg-icon>
<div class="item_text"> <div class="item_text">
{{ t(`firstMenu.${item.name}`) }} {{ t(`firstMenu.${item.name}`) }}
</div> </div>
</div> </div>
</div> </div>
<leftSideSecond class="absolute zIndex99 leftSideSecond" ref="leftSideSecondRef"></leftSideSecond>
</div> </div>
</div> </div>
<div class="left_bottom" @click="fold"></div> <div class="left_bottom" @click="fold"></div>
@ -25,43 +25,88 @@
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { bus } from '@/utils/bus' import { bus } from '@/utils/bus'
import leftSideSecond from '@/views/components/leftSide/leftSideSecond.vue'
const { t } = useI18n() const { t } = useI18n()
const menuList = ref([ const menuList = ref([
// 模型库 // 方案推演
{ {
name: 'situation', name: 'situation',
svg: 'situation', svg: 'situation',
// fun: this.openModel, // fun: this.openModel,
key: 'situation' key: 'situation',
children: []
}, },
// 模型库 // 模型库
{ {
name: 'modelLibrary', name: 'modelLibrary',
svg: 'model', svg: 'model',
// fun: this.openModel, // fun: this.openModel,
key: 'model' key: 'model',
children: []
}, },
// 特效库 // 特效库
{ {
name: 'effect', name: 'effect',
svg: 'special', svg: 'special',
// fun: this.showSecondMenu, // fun: this.showSecondMenu,
key: 'effect' key: 'effect',
children: [
"trajectoryMotion",
"electronicFence",
// "nightVision",
"radarLightWave",
"diffusedLightWave",
"scanStereoscopic",
"multilateralBody",
"waterSurface",
"fountain",
"waterL",
"fire",
"explosion",
"smoke",
"nightVision",
// "nightVision",
]
}, },
// 分析 // 分析
{ {
name: 'analysis', name: 'analysis',
svg: 'analysis', svg: 'analysis',
// fun: this.showSecondMenu, // fun: this.showSecondMenu,
key: 'analysis' key: 'analysis',
children: [
"inundationAnalysis",
"profileAnalysis",
"sightAnalysis",
"kenAnalysis",
"circleKen",
"slopeDirection",
"cutFill",
"contour",
"globalContour",
"clear",
]
}, },
// 测量 // 测量
{ {
name: 'measure', name: 'measure',
svg: 'measure', svg: 'measure',
// fun: this.showSecondMenu, // fun: this.showSecondMenu,
key: 'measure' key: 'measure',
children: [
"projectionArea",
"projectionDistanceMeasure",
"areaMeasure",
"distanceMeasure",
"heightMeasure",
"triangleMeasure",
"MeasureAzimuth",
"MeasureAngle",
"lopeDistanceMeasures",
"coorMeasure",
"clear",
]
}, },
// 工具 // 工具
@ -69,21 +114,44 @@ const menuList = ref([
name: 'tool', name: 'tool',
svg: 'tool', svg: 'tool',
// fun: this.showSecondMenu, // fun: this.showSecondMenu,
key: 'tool' key: 'tool',
children: [
"routePlan",
//清除轨迹
"graffiti",
// stopGraffiti: "结束涂鸦",
"clearGraffiti",
"path",
"coorLocation",
"mouseLocation",
"annotationAggregation",
// 卷帘对比
// 屏幕截图
// 高清出图
// 视频录制
"pressModel",
"terrainDig",
"tilesetClipping",
"clearTilesetClipping",
"projConvert",
"projectionConvert",
"gdbImport",
]
}, },
// 工具
{ {
name: 'militaryMark', name: 'militaryMark',
svg: 'military', svg: 'military',
// fun: this.showSecondMenu, // fun: this.showSecondMenu,
key: 'military' key: 'military',
children: []
}, },
//二三维 //二三维
{ {
name: 'ersanwei', name: 'ersanwei',
// fun: this.map2d, // fun: this.map2d,
svg: 'dimension', svg: 'dimension',
key: 'ersanwei' key: 'ersanwei',
children: []
} }
]) ])
const isFolded: any = ref(false) // 添加折叠状态 const isFolded: any = ref(false) // 添加折叠状态
@ -100,7 +168,17 @@ onMounted(() => {
initialPositions.value[index] = item.style.transform || 'translateX(0)' initialPositions.value[index] = item.style.transform || 'translateX(0)'
}) })
}) })
const leftSideSecondRef = ref()
const handleClick = (item: any, e) => {
console.log("点击了", item, e);
$(".leftSideSecond")[0].style.left = "100%";
$(".leftSideSecond")[0].style.top = e.layerY - 120 + "px"
$(".leftSideSecond")[0].style.display = "none"
if (item.children.length) {
$(".leftSideSecond")[0].style.display = "block"
leftSideSecondRef.value.initList(item)
}
}
const fold = () => { const fold = () => {
// 如果正在动画中,直接返回 // 如果正在动画中,直接返回
if (isAnimating.value) return if (isAnimating.value) return
@ -194,6 +272,8 @@ const fold = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0 8px 0 5px; padding: 0 8px 0 5px;
position: relative;
// margin-top: 20px; // margin-top: 20px;
.menus_itemBox { .menus_itemBox {
width: 100%; width: 100%;
@ -216,6 +296,15 @@ const fold = () => {
align-items: center; align-items: center;
} }
.item_icon:hover .item_text {
color: rgba(0, 255, 255, 1);
}
.item_icon:hover {
background: url('../../../assets/images/hongse/left1.png') no-repeat;
background-size: 100% 100%;
}
.item_text { .item_text {
width: 100%; width: 100%;
font-size: 1.1rem; font-size: 1.1rem;
@ -233,14 +322,9 @@ const fold = () => {
} }
} }
.menus_itemBox:hover .item_icon {
background: url('../../../assets/images/hongse/left1.png') no-repeat;
background-size: 100% 100%;
}
.menus_itemBox:hover .item_text {
color: rgba(0, 255, 255, 1);
}
} }
} }

View File

@ -0,0 +1,162 @@
<template>
<div class="leftSideSecond">
<div class="leftSideSecondBox">
<template v-if="obj">
<div class="menuItem" v-for="value in obj.children" @click="handleClick(value)">
<img :src="'src/assets/images/second/' + `${value}` + '.png'" alt="">
<span>{{ t(`${obj.key}.${value}`) }}</span>
</div>
</template>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { ref, reactive, getCurrentInstance } from "vue"
import { useTreeNode } from '../tree/hooks/treeNode'
import { TreeApi } from '@/api/tree'
import { renderMethods } from '../tree/hooks/renderTreeNode'
const { proxy } = getCurrentInstance()
const { t } = useI18n()
const { findParentId, findTreeIndex } = useTreeNode()
const obj = ref(null)
const initList = (value) => {
obj.value = value
}
const methodMap = {
// 电子围墙
electronicFence: () => {
let draw = new YJ.Draw.DrawPolyline(window.earth);
draw.start((err, positions) => {
if (positions.length > 1) {
let alt = positions[0].alt;
positions.forEach((item) => {
if (item.alt < alt) alt = item.alt;
});
let id = proxy.$md5(new Date().getTime() + "围墙");
let params = {
sourceName: "电子围墙",
id,
sourceType: "wallStereoscopic",
parentId: findParentId(window.treeObj),
params: {
id,
positions,
color: "#fff",
cornerType: undefined,
extrudedHeight: 2.4,
width: 0.24,
},
treeIndex: findTreeIndex(window.treeObj)
}
// console.log(params);
// 渲染电子围墙
renderMethods.renderWallStereoscopic(params)
// 存入数据库
let res = TreeApi.addOtherSource(params)
console.log("addOtherSource", res);
// 上树
cusAddNodes(window.treeObj, params.parentId, [params])
}
})
},
// 扩散光波
radarLightWave: () => {
let draw = new YJ.Draw.DrawCircle(window.earth);
draw.start((err, params) => {
console.log(params);
if (params) {
}
})
},
//投影面积
projectionArea: () => {
new YJ.Measure.MeasureTyArea(window.earth).start();
},
//投影距离测量
projectionDistanceMeasure: () => {
new YJ.Measure.MeasureProjectionDistance(window.earth).start();
},
areaMeasure: () => {
new YJ.Measure.MeasureTdArea(window.earth).start();
},
//距离测量
distanceMeasure: () => {
new YJ.Measure.MeasureDistance(window.earth).start();
},
//高度测量
heightMeasure: () => {
new YJ.Measure.MeasureHeight(window.earth).start();
},
//三角测量
triangleMeasure: () => {
new YJ.Measure.MeasureTriangle(window.earth).start();
},
// 方位角
MeasureAzimuth() {
new YJ.Measure.MeasureAzimuth(window.earth).start();
},
//夹角测量
MeasureAngle() {
new YJ.Measure.MeasureAngle(window.earth).start();
},
// 坡度测量
lopeDistanceMeasures() {
new YJ.Measure.MeasureSlopeDistance(window.earth).start();
},
//坐标测量
coorMeasure() {
new YJ.Measure.MeasureLocation(window.earth).start();
},
//清除测量
clear() {
YJ.Measure.Clear();
},
}
const handleClick = (value = 'projectionDistanceMeasure') => {
methodMap[value]()
}
defineExpose({
initList
})
</script>
<style lang="scss" scoped>
.leftSideSecond {
display: none;
height: 365px;
width: 275px;
background: url("@/assets/images/secondBj.png") no-repeat;
background-size: 100% 100%;
padding: 13px 4px 13px 11px;
.leftSideSecondBox {
border: 1px solid red;
display: flex;
flex-wrap: wrap;
max-height: 100%;
overflow-y: auto;
.menuItem {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
margin: 5px 0;
img {
width: 20px;
height: 20px
}
span {
font-size: 12px;
}
}
}
}
</style>

View File

@ -12,7 +12,7 @@
<div> <div>
<div class="row"> <div class="row">
<div class="col input-select-unit-box"> <div class="col input-select-unit-box">
<el-select v-model="wordsName"> <el-select v-model="entityOptions.wordsName">
<el-option label="空间长度" value="0"></el-option> <el-option label="空间长度" value="0"></el-option>
<el-option label="投影长度" value="1"></el-option> <el-option label="投影长度" value="1"></el-option>
<el-option label="地表长度" value="2"></el-option> <el-option label="地表长度" value="2"></el-option>
@ -202,7 +202,6 @@ const baseDialog = ref(null);
const eventBus = inject("bus"); const eventBus = inject("bus");
const length = ref(0) const length = ref(0)
const wordsName = ref('0')
const lengthUnit = ref('m') const lengthUnit = ref('m')
const fontList = ref(getFontList()) const fontList = ref(getFontList())
const height = ref(10) const height = ref(10)
@ -325,6 +324,14 @@ const open = async (id) => {
heightMode.value = entityOptions.value.heightMode heightMode.value = entityOptions.value.heightMode
length.value = entityOptions.value.lengthByMeter length.value = entityOptions.value.lengthByMeter
linePositions.value = structuredClone(that.options.positions) linePositions.value = structuredClone(that.options.positions)
that.lengthChangeCallBack = ()=>{
if (lengthUnit.value == 'km') {
length.value = entityOptions.value.lengthByMeter / 1000
}
else {
length.value = entityOptions.value.lengthByMeter
}
}
heightModeChange(heightMode.value) heightModeChange(heightMode.value)
baseDialog.value?.open() baseDialog.value?.open()

View File

@ -12,10 +12,10 @@
<div> <div>
<div class="row"> <div class="row">
<div class="col input-select-unit-box"> <div class="col input-select-unit-box">
<el-select v-model="wordsName"> <el-select v-model="entityOptions.wordsName">
<el-option label="空间长度" value="0"></el-option> <el-option label="空间长度" :value="0"></el-option>
<el-option label="投影长度" value="1"></el-option> <el-option label="投影长度" :value="1"></el-option>
<el-option label="地表长度" value="2"></el-option> <el-option label="地表长度" :value="2"></el-option>
</el-select> </el-select>
<input v-model="length" class="input-text" readonly="readonly"> <input v-model="length" class="input-text" readonly="readonly">
<el-select v-model="lengthUnit"> <el-select v-model="lengthUnit">
@ -208,7 +208,6 @@ const baseDialog = ref(null);
const eventBus = inject("bus"); const eventBus = inject("bus");
const length = ref(0) const length = ref(0)
const wordsName = ref('0')
const lengthUnit = ref('m') const lengthUnit = ref('m')
const fontList = ref(getFontList()) const fontList = ref(getFontList())
const height = ref(10) const height = ref(10)
@ -331,6 +330,14 @@ const open = async (id) => {
heightMode.value = entityOptions.value.heightMode heightMode.value = entityOptions.value.heightMode
length.value = entityOptions.value.lengthByMeter length.value = entityOptions.value.lengthByMeter
linePositions.value = structuredClone(that.options.positions) linePositions.value = structuredClone(that.options.positions)
that.lengthChangeCallBack = ()=>{
if (lengthUnit.value == 'km') {
length.value = entityOptions.value.lengthByMeter / 1000
}
else {
length.value = entityOptions.value.lengthByMeter
}
}
heightModeChange(heightMode.value) heightModeChange(heightMode.value)
baseDialog.value?.open() baseDialog.value?.open()

View File

@ -0,0 +1,10 @@
export const renderMethods = {
renderWallStereoscopic: (node: any) => {
console.log("renderWallStereoscopic", node);
let option = {
name: node.sourceName,
...node.params
}
let WallStereoscopic = new YJ.Obj.WallStereoscopic(window.earth, option);
}
}

View File

@ -406,9 +406,11 @@ export const useTree = () => {
// 初始化树的方法 // 初始化树的方法
const initTree = async (selector: string = '#treeDemo') => { const initTree = async (selector: string = '#treeDemo') => {
let res = await TreeApi.getTreeList() let res = await TreeApi.getTreeList()
if (res.code == 0 || res.code == 200) { console.log('res', res)
if ([0,200].includes(res.code)) {
res.data.sort((a: any, b: any) => { res.data.sort((a: any, b: any) => {
return a.tree_index - b.tree_index if(a.treeIndex&&b.treeIndex)
return a.treeIndex - b.treeIndex
}) })
zNodes.value = res.data zNodes.value = res.data
treeObj.value = $.fn.zTree.init($(selector), setting, zNodes.value) treeObj.value = $.fn.zTree.init($(selector), setting, zNodes.value)

View File

@ -1,3 +1,4 @@
import { renderMethods } from "./renderTreeNode";
export const useTreeNode = () => { export const useTreeNode = () => {
//定义树形节点的属性 //定义树形节点的属性
const nodeType = { const nodeType = {
@ -91,10 +92,10 @@ export const useTreeNode = () => {
'importPanorama', 'importPanorama',
'edit', 'edit',
'del' 'del'
] ],
// detailFun: get_detail_null, // detailFun: get_detail_null,
// render: () => {}, // render: () => {},
// allowChildren: true, allowChildren: true,
}, },
tileset: { tileset: {
rightMenus: [ rightMenus: [
@ -172,9 +173,9 @@ export const useTreeNode = () => {
// render: renderScanStereoscopic, // render: renderScanStereoscopic,
}, },
wallStereoscopic: { wallStereoscopic: {
rightMenus: ['edit', 'del', 'setView', 'resetView'] rightMenus: ['edit', 'del', 'setView', 'resetView'],
// detailFun: get_detail_wallStereoscopic, // detailFun: get_detail_wallStereoscopic,
// render: renderWallStereoscopic, render: renderMethods.renderWallStereoscopic,
}, },
entityWall: { entityWall: {
rightMenus: ['edit', 'del', 'setView', 'resetView'] rightMenus: ['edit', 'del', 'setView', 'resetView']
@ -560,6 +561,47 @@ export const useTreeNode = () => {
return arr return arr
} }
function findParentId(treeObj: any) {
if (!treeObj) {
return -1;
}
let selectedNode = getSelectedNode(treeObj);
if (!selectedNode) {
return -1;
}
while (!nodeType[selectedNode.sourceType].allowChildren) {
selectedNode = treeObj.getNodeByParam("id", selectedNode.parentId, null);
if (!selectedNode) {
return -1;
}
}
return selectedNode.id;
}
function findTreeIndex(treeObj: any) {
if (!treeObj) {
return 0;
}
let selectedNode = getSelectedNode(treeObj);
if (!selectedNode) {
return treeObj.getNodes().length;
} else {
// 有选中的节点后添加节点
// 选中的节点允许添加子节点则返回该节点下children的长度
if (nodeType[selectedNode.sourceType].allowChildren) {
let nodes = treeObj.getNodesByParam("parentId", selectedNode.id, null)
return nodes.length
} else {
// 否则返回该节点父节点的children的长度
let parentNode = treeObj.getNodesByParam("id", selectedNode.parentId, null);
if (parentNode) {
return parentNode.children.length
} else
return treeObj.getNodes().length; // 如果没有父节点,则返回根节点的长度
}
}
}
return { return {
showRightMenu, showRightMenu,
@ -570,6 +612,8 @@ export const useTreeNode = () => {
cusAddNodes, cusAddNodes,
getSameLevel, getSameLevel,
cusRemoveNode, cusRemoveNode,
cusUpdateNode cusUpdateNode,
findParentId,
findTreeIndex
} }
} }

View File

@ -36,7 +36,6 @@ export const initMapData = async (type, data) => {
return return
break break
} }
console.log(type, entityObject)
options = structuredClone(entityObject.options) options = structuredClone(entityObject.options)
delete options.host delete options.host
return options return options

View File

@ -1,40 +1,21 @@
<template> <template>
<div <div class="login-container" style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0">
class="login-container" <transition name="video-fade" mode="out-in"
style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0" style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; object-fit: cover">
>
<transition
name="video-fade"
mode="out-in"
style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; object-fit: cover"
>
<!-- 第一个视频播放一次 --> <!-- 第一个视频播放一次 -->
<video <video v-if="!isFirstVideoPlayed" ref="firstVideoRef" key="first-video" muted @ended="onFirstVideoEnded"
v-if="!isFirstVideoPlayed"
ref="firstVideoRef"
key="first-video"
muted
@ended="onFirstVideoEnded"
src="../../assets/video/login_front.mp4" src="../../assets/video/login_front.mp4"
style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; object-fit: cover" style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; object-fit: cover"></video>
></video>
<!-- 第二个视频循环播放 --> <!-- 第二个视频循环播放 -->
<video <video v-else key="second-video" autoplay loop muted src="../../assets/video/login_feature.mp4"
v-else style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; object-fit: cover"></video>
key="second-video"
autoplay
loop
muted
src="../../assets/video/login_feature.mp4"
style="position: fixed; width: 100vw; height: 100vh; left: 0; top: 0; object-fit: cover"
></video>
</transition> </transition>
<div class="rightBox" v-if="isDesktop"> <div class="rightBox" v-if="isDesktop">
<el-button size="small" :icon="Setting" @click="serviceDialog = true"> </el-button> <el-button size="small" :icon="Setting" @click="serviceDialog = true"> </el-button>
<el-button size="small" :icon="SwitchButton" @click="goExit"> </el-button> <el-button size="small" :icon="SwitchButton" @click="goExit"> </el-button>
</div> </div>
<transition name="fade"> <transition name="fade">
<div v-show="showContent" class="content"> <div v-show="true" class="content">
<div class="titleBox"> <div class="titleBox">
<div class="titleItem"> <div class="titleItem">
<div> <div>
@ -43,45 +24,21 @@
</div> </div>
</div> </div>
</div> </div>
<el-form <el-form class="login-form" autoComplete="on" :model="loginForm" :rules="loginRules" ref="loginFormRef"
class="login-form" label-position="left">
autoComplete="on"
:model="loginForm"
:rules="loginRules"
ref="loginFormRef"
label-position="left"
>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input name="username" type="text" v-model="loginForm.username" autoComplete="on" placeholder="请输入用户名"
name="username" :prefix-icon="User" />
type="text"
v-model="loginForm.username"
autoComplete="on"
placeholder="请输入用户名"
:prefix-icon="User"
/>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input type="password" @keyup.enter.native="handleLogin(loginFormRef)" v-model="loginForm.password"
type="password" autoComplete="on" placeholder="请输入密码" :prefix-icon="Unlock" show-password></el-input>
@keyup.enter.native="handleLogin(loginFormRef)"
v-model="loginForm.password"
autoComplete="on"
placeholder="请输入密码"
:prefix-icon="Unlock"
show-password
></el-input>
</el-form-item> </el-form-item>
<el-form-item class="rememberForget"> <el-form-item class="rememberForget">
<!-- justify-content: space-around;align-items: center; --> <!-- justify-content: space-around;align-items: center; -->
<div style="display: flex"> <div style="display: flex">
<el-checkbox <el-checkbox :disabled="loading" v-model="checkboxVModel" @change="rememberpwd"
:disabled="loading" label="string">记住密码</el-checkbox>
v-model="checkboxVModel"
@change="rememberpwd"
label="string"
>记住密码</el-checkbox
>
<!-- <div style="cursor: pointer;">忘记密码</div> --> <!-- <div style="cursor: pointer;">忘记密码</div> -->
</div> </div>
</el-form-item> </el-form-item>
@ -105,11 +62,8 @@
<div class="serviceContent"> <div class="serviceContent">
<div class="tab"> <div class="tab">
<template v-for="item in serviceOptions"> <template v-for="item in serviceOptions">
<span <span :class="['tab-item', selectedService == item.name ? 'activeService' : '']"
:class="['tab-item', selectedService == item.name ? 'activeService' : '']" @click="selectedService = item.name">{{ item.name }}</span>
@click="selectedService = item.name"
>{{ item.name }}</span
>
</template> </template>
</div> </div>
<div class="tabPanel"> <div class="tabPanel">
@ -117,19 +71,13 @@
<div class="item"> <div class="item">
<span class="itemLabel">服务选择</span> <span class="itemLabel">服务选择</span>
<el-select class="select" v-model="servVal"> <el-select class="select" v-model="servVal">
<el-option <el-option size="mini" v-for="item in servOptions" :key="item.value" :label="item.name"
size="mini" :value="item.name">
v-for="item in servOptions"
:key="item.value"
:label="item.name"
:value="item.name"
>
</el-option> </el-option>
</el-select> </el-select>
</div> </div>
<div class="prototype" v-if="servVal == '网络'"> <div class="prototype" v-if="servVal == '网络'">
<span class="itemLabel">协议</span <span class="itemLabel">协议</span><el-input style="width: 200px" v-model="prototype"></el-input>
><el-input style="width: 200px" v-model="prototype"></el-input>
</div> </div>
<div class="item ip"> <div class="item ip">
<template v-if="servVal == '单机'"> <template v-if="servVal == '单机'">
@ -155,13 +103,8 @@
<div class="item"> <div class="item">
<span class="itemLabel">串口选择</span> <span class="itemLabel">串口选择</span>
<el-select class="select" v-model="gpsVal"> <el-select class="select" v-model="gpsVal">
<el-option <el-option size="mini" v-for="item in gpsOptions" :key="item.value" :label="item.Product"
size="mini" :value="item.Name">
v-for="item in gpsOptions"
:key="item.value"
:label="item.Product"
:value="item.Name"
>
</el-option> </el-option>
</el-select> </el-select>
</div> </div>
@ -245,44 +188,58 @@ onMounted(() => {
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }
.content { .content {
.el-input__wrapper { .el-input__wrapper {
background-color: transparent; background-color: transparent;
border: 0.2px solid #00ffff; border: 0.2px solid #00ffff;
box-shadow: 0 0 0 0.2px #00ffff inset !important; /* 新增此行 */ box-shadow: 0 0 0 0.2px #00ffff inset !important;
/* 新增此行 */
} }
.el-input__inner { .el-input__inner {
color: #fff; color: #fff;
} }
/* 新增图标颜色控制 */ /* 新增图标颜色控制 */
.el-input__prefix, .el-input__prefix,
.el-input__suffix { .el-input__suffix {
.el-icon { .el-icon {
color: #00ffff; /* 将#00ffff替换为你想用的颜色 */ color: #00ffff;
font-size: 18px; /* 可选:调整图标大小 */ /* 将#00ffff替换为你想用的颜色 */
font-size: 18px;
/* 可选:调整图标大小 */
} }
&:hover .el-icon { &:hover .el-icon {
color: #00ffff; /* 悬停颜色保持一致 */ color: #00ffff;
/* 悬停颜色保持一致 */
} }
} }
.el-checkbox__label { .el-checkbox__label {
color: #fff; // 使用与边框一致的青蓝色 color: #fff; // 使用与边框一致的青蓝色
font-size: 1rem; // 可选字体大小调整 font-size: 1rem; // 可选字体大小调整
} }
// // 保持悬停状态颜色一致 // // 保持悬停状态颜色一致
// .el-checkbox:hover .el-checkbox__label { // .el-checkbox:hover .el-checkbox__label {
// color: #fff; // color: #fff;
// } // }
} }
.service { .service {
.el-input__wrapper { .el-input__wrapper {
background-color: transparent; background-color: transparent;
border: 0.2px solid #00ffff; border: 0.2px solid #00ffff;
box-shadow: 0 0 0 0.2px #00ffff inset !important; /* 新增此行 */ box-shadow: 0 0 0 0.2px #00ffff inset !important;
/* 新增此行 */
} }
.el-input__inner { .el-input__inner {
color: #fff; color: #fff;
} }
// .el-checkbox__label { // .el-checkbox__label {
// color: #fff; // 使用与边框一致的青蓝色 // color: #fff; // 使用与边框一致的青蓝色
// // font-size: 0.8rem; // 可选字体大小调整 // // font-size: 0.8rem; // 可选字体大小调整
@ -294,8 +251,10 @@ onMounted(() => {
.el-select__wrapper { .el-select__wrapper {
background-color: transparent; background-color: transparent;
border: 0.2px solid #00ffff; border: 0.2px solid #00ffff;
box-shadow: 0 0 0 0.2px #00ffff inset !important; /* 新增此行 */ box-shadow: 0 0 0 0.2px #00ffff inset !important;
/* 新增此行 */
} }
.el-select__placeholder, .el-select__placeholder,
.el-select__tags-text { .el-select__tags-text {
color: #fff !important; color: #fff !important;
@ -388,6 +347,7 @@ onMounted(() => {
} }
} }
} }
.rightBox { .rightBox {
position: absolute; position: absolute;
right: 10px; right: 10px;
@ -400,6 +360,7 @@ onMounted(() => {
color: #fff; color: #fff;
} }
} }
.service { .service {
z-index: 999; z-index: 999;
width: 100vw; width: 100vw;
@ -441,7 +402,7 @@ onMounted(() => {
border-radius: 0 0 0 90%; border-radius: 0 0 0 90%;
background: #00ffff; background: #00ffff;
& > span { &>span {
font-size: 1rem; font-size: 1rem;
position: absolute; position: absolute;
right: 5px; right: 5px;
@ -521,8 +482,10 @@ onMounted(() => {
clip-path: polygon(0 0, calc(100% - 3px) 0, 100% 6px, 0 6px); clip-path: polygon(0 0, calc(100% - 3px) 0, 100% 6px, 0 6px);
} }
} }
.btn { .btn {
text-align: center; text-align: center;
button { button {
border: 1px solid rgba(0, 255, 255, 0.5) !important; border: 1px solid rgba(0, 255, 255, 0.5) !important;
background: rgba(0, 255, 255, 0.2) !important; background: rgba(0, 255, 255, 0.2) !important;

View File

@ -14,18 +14,18 @@ export const useLogin = () => {
} }
// 新增监听逻辑 // 新增监听逻辑
watch(isFirstVideoPlayed, (newVal) => { // watch(isFirstVideoPlayed, (newVal) => {
if (newVal) { // if (newVal) {
if (newVal) { // if (newVal) {
// 延迟一秒显示 // // 延迟一秒显示
setTimeout(() => { // setTimeout(() => {
showContent.value = true // showContent.value = true
}, 800) // }, 800)
} else { // } else {
showContent.value = false // showContent.value = false
} // }
} // }
}) // })
const firstVideoRef = ref<HTMLVideoElement | null>(null) // 引用第一个视频元素 const firstVideoRef = ref<HTMLVideoElement | null>(null) // 引用第一个视频元素
// 登录表单引用 // 登录表单引用
const loginFormRef = ref<FormInstance>() // 用于获取表单实例 const loginFormRef = ref<FormInstance>() // 用于获取表单实例
@ -70,13 +70,15 @@ export const useLogin = () => {
try { try {
loading.value = true loading.value = true
const res = await LoginApi.login(loginForm.value) const res = await LoginApi.login(loginForm.value)
if (res.code === 0) { console.log(res);
if ([0,200].includes(res.code) ) {
checkboxVModel.value = true checkboxVModel.value = true
localStorage.setItem('access_token', res.data.token) localStorage.setItem(res.data.header, res.data.token)
localStorage.setItem( localStorage.setItem(
'userInfo', 'userInfo',
JSON.stringify({ JSON.stringify({
...res.data.userInfo, // ...res.data.userInfo,
...loginForm.value, ...loginForm.value,
checkboxVModel: checkboxVModel.value checkboxVModel: checkboxVModel.value
}) })