同步admin

This commit is contained in:
CQ 2026-01-31 11:23:56 +08:00
parent 7581ceb5a9
commit 5a2da25c9c
45 changed files with 1054 additions and 312 deletions

View File

@ -47,6 +47,7 @@
"@typescript-eslint/eslint-plugin": "5.53.0",
"@vitejs/plugin-vue": "4.0.0",
"autoprefixer": "10.4.13",
"cross-env": "^7.0.3",
"eslint": "8.34.0",
"eslint-config-standard-with-typescript": "34.0.0",
"eslint-plugin-import": "2.27.5",
@ -2616,6 +2617,24 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",

View File

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && node publish.cjs",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 && vite build && node publish.cjs",
"preview": "vite preview"
},
"dependencies": {
@ -60,6 +60,7 @@
"unplugin-auto-import": "0.13.0",
"unplugin-vue-components": "0.23.0",
"vite": "4.1.0",
"vue-tsc": "1.0.24"
"vue-tsc": "1.0.24",
"cross-env": "^7.0.3"
}
}

View File

@ -60,6 +60,13 @@ export function getWeappLastCommitRecord() {
return request.get('wxoplatform/weapp/commit/last')
}
/**
*
*/
export function delVersion(id: number) {
return request.delete(`wxoplatform/weapp/version/${id}`)
}
/**
*
* @returns

View File

@ -34,37 +34,14 @@
</div>
</template>
</template>
<div v-if="step == 2">
<el-steps :active="numberOfSteps" align-center class="number-of-steps" process-status="process" v-if="!errorDialog && active != 'complete'">
<div v-show="step == 2">
<el-steps :active="numberOfSteps" align-center class="number-of-steps" process-status="process">
<el-step :title="t('testDirectoryPermissions')" />
<el-step :title="t('upgrade.option')" />
<el-step :title="t('startUpgrade')" />
<el-step :title="t('upgradeEnd')" />
</el-steps>
<div class="h-[400px]" style="overflow: auto">
<!-- <div class="time-dialog-wrap mt-[30px]" v-show="active == 'content'">-->
<!-- <el-timeline style="width: 100%">-->
<!-- <el-timeline-item v-for="(item, index) in upgradeContent.version_list" :key="index" placement="left">-->
<!-- <div class="relative">-->
<!-- <span class="text-[#333333] text-[16px] absolute">{{ timeSplit(item.release_time)[0] }}</span>-->
<!-- <br />-->
<!-- <span class="text-[#999999] text-[14px] w-[78px] block mt-[10px] absolute" style="text-align: right"> {{ timeSplit(item.release_time)[1] }}</span>-->
<!-- </div>-->
<!-- <el-collapse v-model="activeName" accordion>-->
<!-- <el-collapse-item :name="index">-->
<!-- <template #title>-->
<!-- <span class="text-[#333] text-[16px]"> v{{ item.version_no }} </span>-->
<!-- </template>-->
<!-- <div class="px-[20px] py-[20px] bg-overlay timeline-log-wrap whitespace-pre-wrap rounded-[4px]" style="background: rgba(25, 103, 249, 0.03);" v-if="item['upgrade_log']">-->
<!-- <div v-html="item['upgrade_log']"></div>-->
<!-- </div>-->
<!-- </el-collapse-item>-->
<!-- </el-collapse>-->
<!-- </el-timeline-item>-->
<!-- </el-timeline>-->
<!-- </div>-->
<!-- 判断文件权限 -->
<div v-show="active == 'upgrade'">
<div class="flex flex-col" v-if="upgradeCheck && !upgradeTask">
@ -132,7 +109,6 @@
<div class="h-[370px] mt-[30px]" v-show="showTerminal && upgradeTask && !errorDialog">
<terminal ref="terminalRef" :name="`upgrade-${terminalId}`" :context="upgradeTask ? upgradeTask.upgrade.app_key : ''" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd" />
</div>
</div>
<!-- 是否备份选择 -->
<div class="flex flex-col" v-show="active == 'backup'">
@ -156,36 +132,63 @@
</div>
</el-scrollbar>
</div>
<div class="mt-[20px] h-[370px]" v-show="errorDialog">
<el-result icon="error" :title="t('升级失败')">
<template #icon>
<img src="@/app/assets/images/error_icon.png" alt="" />
</template>
<template #extra>
<el-scrollbar class="max-h-[120px] !overflow-auto text-[15px] text-[#4F516D] mb-[15px] mt-[-15px]">
{{errorMsg}}
</el-scrollbar>
<el-button @click="handleBack()" class="!w-[90px]">错误信息</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
<div class="mt-[20px]" v-show="active == 'complete'">
<el-result icon="success" :title="t('upgrade.upgradeSuccess')">
<template #icon>
<img src="@/app/assets/images/success_icon.png" alt="">
</template>
<template #extra>
<div class="text-[16px] text-[#4F516D] mt-[-5px]" v-show="upgradeTask && upgradeTask.executed && !upgradeTask.executed.includes('cloudBuild')">{{ t('upgrade.upgradeCompleteTips') }}</div>
<div class="text-[16px] text-[#9699B6] mt-[10px]">本次升级用时{{ formatUpgradeDuration }}</div>
<div class="mt-[20px]">
<el-button @click="handleBack()" class="!w-[90px]">返回</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</div>
</div>
<div v-if="step == 3">
<div class="mt-[10px]" v-show="active == 'complete'">
<el-result icon="success" :title="t('upgrade.upgradeSuccess')">
<template #icon>
<img src="@/app/assets/images/success_icon.png" alt="">
</template>
<template #extra>
<div class="text-[16px] text-[#4F516D] mt-[-5px]" v-show="upgradeTask && upgradeTask.executed && !upgradeTask.executed.includes('cloudBuild')">{{ t('upgrade.upgradeCompleteTips') }}</div>
<!-- <div class="text-[16px] text-[#9699B6] mt-[10px]">本次升级用时{{ formatUpgradeDuration }}</div> -->
<div class="w-[750px]" v-if="upgradeTask.cloud_build_error">
<el-alert class="!w-[750px] border-warning !border-[1px] !rounded-[0px] border-solid" type="warning" :closable="false">
<template #title>
<span class="text-error">警告</span>
<span class="text-black">升级过程中发生云编译错误</span>
</template>
</el-alert>
<div class="text-left mt-[10px] leading-8">
<div class="font-bold">为了保证系统稳定建议您做以下处理:</div>
<div><span class="w-[6px] h-[6px] rounded-[6px] bg-black inline-block mr-[10px]"></span>如果您是开发者安装的框架或者插件二开过或者正在开发中可能是因为您的本地代码不完整导致的云编译失败需要自己调试并重新进行编译才算升级完成(云编译会把本地插件前端代码上传编译)</div>
<div><span class="w-[6px] h-[6px] rounded-[6px] bg-black inline-block mr-[10px]"></span>如果您没有二开过任何代码可能是本地插件存在兼容性问题请联系插件开发者或者官方客服解决</div>
<div><span class="w-[6px] h-[6px] rounded-[6px] bg-black inline-block mr-[10px]"></span>如果您的项目已经投入正式运营中请立即回滚</div>
</div>
</template>
</el-result>
<!-- <el-alert :title="t('upgrade.upgradeCompleteTips')" type="error" :closable="false" v-show="upgradeTask && upgradeTask.executed && !upgradeTask.executed.includes('cloudBuild')"/> -->
</div>
<div class="text-left mt-[10px]">
<div class="font-bold">编译信息错误</div>
<div class="mt-[10px] text-secondary overflow-hidden line-clamp-4">
{{ upgradeTask.cloud_build_error }}
</div>
</div>
<div class="mt-[20px]">
<el-button @click="handleBack()" class="!w-[90px]">更多信息</el-button>
<el-button @click="showDialog=false" type="primary" plain class="!w-[90px]">我已知晓</el-button>
<el-button @click="cloudBuildError('cloud_build_error_rollback')" type="primary" class="!w-[90px]">回滚</el-button>
</div>
</div>
<div class="mt-[20px]" v-else>
<el-button @click="handleBack()" class="!w-[90px]">返回</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</div>
</template>
</el-result>
</div>
<div class="mt-[20px] h-[370px]" v-show="active == 'fail'">
<el-result icon="error" :title="t('升级失败')">
<template #icon>
<img src="@/app/assets/images/error_icon.png" alt="" />
</template>
<template #extra>
<el-scrollbar class="max-h-[120px] !overflow-auto text-[15px] text-[#4F516D] mb-[15px] mt-[-15px]">
{{errorMsg}}
</el-scrollbar>
<el-button @click="handleBack()" class="!w-[90px]">错误信息</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
</div>
</template>
@ -354,8 +357,6 @@ const getUpgradeTaskFn = () => {
errorMsg.value = item
}
})
errorDialog.value = true
showTerminal.value = false
if (upgradeTimer) {
clearInterval(upgradeTimer)
upgradeTimer = null
@ -365,12 +366,17 @@ const getUpgradeTaskFn = () => {
//
if (data.step == 'restoreComplete') {
flashInterval && clearInterval(flashInterval)
step.value = 3
active.value = 'fail'
return
}
//
if (data.step == 'upgradeComplete') {
if (data.cloud_build_error) {
terminalRef.value.pushMessage({ content: data.cloud_build_error, class: 'error' })
}
step.value = 3
active.value = 'complete'
showTerminal.value = false
numberOfSteps.value = 4
notificationEl && notificationEl.close()
emits('complete')
@ -379,12 +385,13 @@ const getUpgradeTaskFn = () => {
upgradeTimer = null
}
timeloading.value = false
clearUpgradeTask()
return
}
numberOfSteps.value = 2
active.value = 'upgrade'
executeUpgradeFn()
}).catch((err) => {
console.log(err)
})
}
@ -395,6 +402,7 @@ const handleBack = () => {
isBack.value = true
showTerminal.value = true
errorDialog.value = false //
step.value = 2
}
const formatUpgradeDuration = computed(() => {
@ -646,6 +654,12 @@ const cloudBuildError = (event: string) => {
getUpgradeTaskFn()
})
break
case 'cloud_build_error_rollback':
upgradeUserOperate(event).then(() => {
handleBack()
executeUpgradeFn()
})
break
}
}

View File

@ -45,5 +45,6 @@
"againUpload": "重新上传",
"uploadWeapp": "上传小程序",
"undoAudit" : "撤回审核",
"undoAuditTips" : "撤回代码审核,单个账号每天审核撤回次数最多不超过 5 次每天的额度从0点开始生效一个月不超过 10 次。是否要继续撤回?"
"undoAuditTips" : "撤回代码审核,单个账号每天审核撤回次数最多不超过 5 次每天的额度从0点开始生效一个月不超过 10 次。是否要继续撤回?",
"helpInfo": "查看帮助"
}

View File

@ -32,5 +32,5 @@
"safeModeTips": "安全模式下,消息包为纯密文,需要开发者加密和解密,安全系数高",
"wechatBaseUri": "借权域名",
"wechatBaseUriPlaceholder": "",
"wechatBaseUriTips": "默认留空填写后将替换https://open.weixin.gg.com/获取授权!"
"wechatBaseUriTips": "默认留空填写后将替换https://open.weixin.qq.com/获取授权!"
}

View File

@ -243,7 +243,7 @@
"carouselSearchSwiperSet": "轮播图设置",
"carouselSearchSwiperControl": "展示开关",
"carouselSearchSwiperInterval": "切换间隔 / 秒",
"carouselSearchSwiperTips": "建议上传尺寸相同的图片推荐尺寸750*350鼠标拖拽可调整图片顺序",
"carouselSearchSwiperTips": "建议上传尺寸相同的图片推荐尺寸750*580鼠标拖拽可调整图片顺序",
"carouselSearchTabStyle": "选项卡样式",
"carouselSearchStyle": "搜索框样式",
"noColor": "常规颜色",

View File

@ -244,7 +244,7 @@
"carouselSearchSwiperSet": "轮播图设置",
"carouselSearchSwiperControl": "展示开关",
"carouselSearchSwiperInterval": "切换间隔 / 秒",
"carouselSearchSwiperTips": "建议上传尺寸相同的图片推荐尺寸750*350鼠标拖拽可调整图片顺序",
"carouselSearchSwiperTips": "建议上传尺寸相同的图片推荐尺寸750*580鼠标拖拽可调整图片顺序",
"carouselSearchTabStyle": "选项卡样式",
"carouselSearchStyle": "搜索框样式",
"noColor": "常规颜色",

View File

@ -4,5 +4,9 @@
"admin": "平台端",
"adminBgImgTip": "建议上传尺寸为450*400px",
"site": "站点端",
"siteBgImgTip": "建议上传尺寸为620*980px"
"siteBgImgTip": "建议上传尺寸为620*980px",
"siteLoginLogo": "站点登录Logo",
"siteLoginLogoTips": "站点端登录Logo建议图片尺寸132*40像素图片格式jpg、png、jpeg。",
"siteLoginBgImg": "站点登录背景图",
"siteLoginBgImgTips": "站点端登录背景图建议图片尺寸1920*1280像素图片格式jpg、png、jpeg。"
}

View File

@ -48,5 +48,6 @@
"publicType": "公众平台类型",
"siteName": "站点名称",
"authTime": "授权时间",
"qrcode": "二维码"
"qrcode": "二维码",
"delWeappVersionTips": "是否确定要删除该版本?"
}

View File

@ -74,5 +74,9 @@
"foldText":"展开/折叠",
"appName": "套餐内含应用",
"addonName": "套餐内含插件",
"siteInitTips":"确定要初始化站点吗?该操作将删除站点内所有数据,该操作无法退回,确定要继续初始化吗?"
"siteInitTips":"确定要初始化站点吗?该操作将删除站点内所有数据,该操作无法退回,确定要继续初始化吗?",
"createTimeStartDate": "开始时间(创建时间)",
"createTimeEndDate": "结束时间(创建时间)",
"expireTimeStartDate": "开始时间(到期时间)",
"expireTimeEndDate": "结束时间(到期时间)"
}

View File

@ -24,5 +24,6 @@
"startBackUp": "开始备份",
"backUpEnd": "备份完成",
"remark": "备注",
"remarkEmpty": "无"
"remarkEmpty": "无",
"status": "状态"
}

View File

@ -13,5 +13,6 @@
"syncTemplateError": "未能同步到模板库",
"templateID": "模板ID",
"siteWeappSync": "站点小程序同步",
"syncSiteWeappTips": "是否要给该套餐下已授权小程序的站点提交代码?"
"syncSiteWeappTips": "是否要给该套餐下已授权小程序的站点提交代码?",
"helpInfo": "查看帮助"
}

View File

@ -70,7 +70,13 @@ getAppList()
const toLink = (item: any) => {
if (item.url) {
router.push(item.url)
// is_target=true
if (item.url.indexOf('is_target=true') != -1) {
const url = router.resolve(item.url)
window.open(url.href)
} else {
router.push(item.url)
}
} else {
addonIndexRoute[item.key] && router.push({ name: addonIndexRoute[item.key] })
}

View File

@ -24,10 +24,10 @@
<el-form-item :label="t('parentMenu')" prop="parent_key">
<el-tree-select class="input-width" v-if="formData.addon != ''" v-model="formData.parent_key"
:props="{ label: 'menu_name', value: 'menu_key' }" :data="addonMenuList" check-strictly
:props="{ label: 'menu_name', value: 'menu_key' }" :data="addonMenuListWithDisabled" check-strictly
:render-after-expand="false" />
<el-tree-select class="input-width" v-else v-model="formData.parent_key"
:props="{ label: 'menu_name', value: 'menu_key' }" :data="sysMenuList" check-strictly
:props="{ label: 'menu_name', value: 'menu_key' }" :data="sysMenuListWithDisabled" check-strictly
:render-after-expand="false" />
</el-form-item>
@ -184,6 +184,38 @@ const getAddonMenuFn = async (key: any) => {
addonMenuList.value = data
}
const markMenuDisabled = (menuList: any[], menuType: number): any[] => {
return menuList.map(item => {
const newItem = { ...item }
//
if (item.menu_key === '') {
// sysMenuList
newItem.disabled = menuType === 2 //
} else {
if (menuType === 2) {
// menu_type === 1
newItem.disabled = item.menu_type !== 1
} else {
// / menu_type === 0
newItem.disabled = item.menu_type !== 0
}
}
// children
if (Array.isArray(item.children) && item.children.length > 0) {
newItem.children = markMenuDisabled(item.children, menuType)
}
return newItem
})
}
const sysMenuListWithDisabled = computed(() => {
return markMenuDisabled(sysMenuList.value, formData.menu_type)
})
const addonMenuListWithDisabled = computed(() => {
return markMenuDisabled(addonMenuList.value, formData.menu_type)
})
//
const addonChange = async (val: any) => {
formData.parent_key = ''

View File

@ -19,13 +19,19 @@
<el-form-item :label="t('url')" prop="url">
<el-input v-model.trim="sysUserLogTableData.searchParam.url" :placeholder="t('urlPlaceholder')" />
</el-form-item>
<el-form-item :label="t('操作时间')" prop="create_time">
<el-date-picker v-model="sysUserLogTableData.searchParam.create_time" type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss" :start-placeholder="t('startDate')"
:end-placeholder="t('endDate')" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadSysUserLogList()">{{ t('search') }}</el-button>
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
</el-form-item>
</el-form>
<div class="flex justify-end items-center w-[20%]">
<div class="flex justify-end items-center w-[10%]">
<div>
<el-button type="primary" class="w-[100px]" @click="clearEvent()">{{ t('清空日志') }}</el-button>
</div>
@ -83,7 +89,8 @@ const sysUserLogTableData = reactive({
searchParam: {
ip: '',
username: '',
url: ''
url: '',
create_time:''
}
})

View File

@ -135,6 +135,14 @@
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
<div v-html="failReason"></div>
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="helpInfo">{{ t('helpInfo') }}</el-button>
<el-button @click="failReasonDialogVisible = false">
{{ t('close') }}
</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="uploadSuccessShowDialog" :title="t('warning')" width="500px" draggable>
@ -470,6 +478,10 @@ const handleFailReason = (data: any) => {
failReasonDialogVisible.value = true
}
const helpInfo = () => {
window.open('https://doc.niucloud.com/saasUse.html?keywords=/configFAQ/minWaChatUpload')
}
const knownToKnow = () => {
Storage.set({ key: 'weappUploadTipsLock', data: true })
uploadSuccessShowDialog.value = false

View File

@ -30,8 +30,8 @@
<div class="w-[700px]">
<div class="flex flex-wrap">
<!-- 多应用切换启动页 -->
<el-button type="primary" @click="showDialog = true" v-if="siteApps.length > 1">{{ t('changePage') }}</el-button>
<!-- 多应用切换启动页 v-if="siteApps.length > 1" -->
<el-button type="primary" @click="showDialog = true">{{ t('changePage') }}</el-button>
<el-button type="primary" @click="toDecorate()" v-show="page.use_template.action == 'decorate'" class="ml-[12px]">{{ t('decorate') }}</el-button>
</div>

View File

@ -115,7 +115,7 @@
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="formMemberList.page" v-model:page-size="formMemberList.limit"
layout="total, sizes, prev, pager, next, jumper" :total="formMemberList.total"
@size-change="getFormRecordsMemberFn()" @current-change="getFormRecordsMemberFn()" />
@size-change="getFormRecordsMemberFn()" @current-change="getFormRecordsMemberFn" />
</div>
</el-tab-pane>

View File

@ -203,11 +203,11 @@
{{ item.site_group.group_name }}
</div>
<el-scrollbar class="flex pb-[20px] pt-[4px] box-border !h-[260px] w-[100%] scrollbarBox">
<div class="flex mx-[30px] mt-[14px] leading-[1] items-center w-full" v-for="app in item.site_group.app_name">
<div class="flex mx-[30px] mt-[14px] leading-[1] items-center" v-for="app in item.site_group.app_name">
<div class="nc-iconfont nc-icon-duiV6mm text-[#466CEA]"></div>
<div class="text-[14px] text-[#666666] ml-[3px] truncate">{{ app }}</div>
</div>
<div class="flex mx-[30px] mt-[14px] leading-[1] text-center w-full" v-for="addon in item.site_group.addon_name">
<div class="flex mx-[30px] mt-[14px] leading-[1] text-center" v-for="addon in item.site_group.addon_name">
<div class="nc-iconfont nc-icon-duiV6mm text-[#466CEA]"></div>
<div class="text-[14px] text-[#666666] ml-[3px] truncate">{{ addon }}</div>
</div>
@ -363,6 +363,7 @@ const selectSite = (site: any) => {
}
const toHome = () => {
window.localStorage.setItem('admin.siteId', 0)
if (!window.localStorage.getItem('admin.token')) {
window.localStorage.setItem('admin.token', getToken())
window.localStorage.setItem('admin.comparisonTokenStorage', getToken())

View File

@ -62,9 +62,9 @@
</el-card>
<div class="flex mb-4 flex-wrap" v-show="showType == 'card'" v-if="localList[activeName].length && !loading">
<div class="rounded-md border p-[16px] pr-[20px] app-card mb-[20px] ml-[20px] cursor-pointer"
<div class="rounded-md border p-[16px] app-card mb-[20px] ml-[20px] cursor-pointer"
@click="appKeySingleSelect($event, row.key)"
:class="{'border-primary': batchUpgradeApp.includes(row.key)}" v-for="row in localList[activeName]" :key="row.key">
:class="{'selected': batchUpgradeApp.includes(row.key)}" v-for="row in localList[activeName]" :key="row.key">
<div class="flex justify-between mb-2">
<div class="flex items-center flex-1 w-0">
@ -87,9 +87,24 @@
<p class="text-xs text-gray-500 truncate" :title="row.key">{{ row.key }}</p>
</div>
</div>
<el-checkbox @click.stop :model-value="batchUpgradeApp.includes(row.key)" :value="row.key" @change="appKeySingleSelect($event, row.key)" class="!w-[14px] !h-[14px]" v-if="activeName == 'recentlyUpdated' || activeName == 'uninstalled'"></el-checkbox>
<view class="w-[60px]">
<template v-if="!row.is_download">
<el-button type="primary" class="flex-1" :loading="downloading == row.key" :disabled="downloading != ''" @click.stop="downEvent(row)">下载</el-button>
</template>
<template v-else-if="!row.install_info || Object.keys(row.install_info).length == 0">
<el-button type="primary" class="flex-1" @click.stop="installAddonFn(row.key)">安装</el-button>
</template>
<template v-else>
<el-button type="warning" class="flex-1" @click.stop="upgradeAddonFn(row.key)" v-if="row.install_info.version != row.version">
更新
</el-button>
<el-button class="flex-1" :disabled="true" v-else>
最新
</el-button>
</template>
</view>
</div>
<div class="flex justify-between">
<div class="flex justify-between items-center">
<div class="text-base">
<span>版本: </span>
<span>{{ row.install_info && Object.keys(row.install_info)?.length ? row.install_info.version : row.version }}</span>
@ -98,26 +113,17 @@
<span class="text-warning">{{ row.version }}</span>
</template>
</div>
<el-button type="primary" link @click.stop="updateInformationFn(row)">更新记录</el-button>
</div>
<div class="flex mt-[20px]">
<template v-if="!row.is_download">
<el-button type="primary" class="flex-1" :loading="downloading == row.key" :disabled="downloading != ''" @click.stop="downEvent(row)"><i class="iconfont iconanzhuang1 mr-[5px]"></i>立即下载</el-button>
</template>
<template v-else-if="!row.install_info || Object.keys(row.install_info).length == 0">
<el-button type="primary" class="flex-1" @click.stop="installAddonFn(row.key)"><i class="iconfont iconanzhuang1 mr-[5px]"></i>立即安装</el-button>
<el-button plain @click.stop="deleteAddonFn(row.key)">删除</el-button>
</template>
<template v-else>
<el-button type="warning" class="flex-1" @click.stop="upgradeAddonFn(row.key)" v-if="row.install_info.version != row.version">
<i class="iconfont icongengxin mr-[5px]"></i>立即更新
</el-button>
<el-button class="flex-1" :disabled="true" v-else>
<el-icon class="mr-[5px]"><Check /></el-icon>已是最新
</el-button>
<el-button plain @click.stop="uninstallAddonFn(row.key)">卸载</el-button>
</template>
<el-dropdown class="ml-[12px]">
<i class="iconfont iconzhankai !text-xs"></i>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="getAddonDetailFn(row)">{{ t("detail") }}</el-dropdown-item>
<el-dropdown-item @click="updateInformationFn(row)">更新记录</el-dropdown-item>
<el-dropdown-item v-if="row.is_download && (!row.install_info || !Object.keys(row.install_info).length)" @click="deleteAddonFn(row.key)">删除</el-dropdown-item>
<el-dropdown-item v-if="row.is_download && row.install_info && Object.keys(row.install_info).length" @click="uninstallAddonFn(row.key)">卸载</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
@ -446,7 +452,14 @@
</div>
</div>
<div v-show="installStep == 1 && !errorDialog" class="h-[50vh] mt-[20px]">
<terminal ref="terminalRef" :name="`install-${terminalId}`" :context="currAddon" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd" />
<div class="flex flex-col h-full">
<div class="flex-1 h-0">
<terminal ref="terminalRef" :name="`install-${terminalId}`" :context="currAddon" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd" />
</div>
<div class="flex justify-end mt-[20px]">
<el-button type="primary" :loading="true" class="!w-[140px]">已用时 {{ formatUpgradeDuration }}</el-button>
</div>
</div>
</div>
<div v-show="installStep == 2" class="h-[50vh] mt-[20px] flex flex-col">
<!-- <el-result icon="success" :title="t('addonInstallSuccess')"></el-result> -->
@ -639,6 +652,8 @@ const downEventHintFn = () => {
downEvent(currDownData.value, true)
}
const batchUpgradeApp = ref<String[]>([])
const activeNameTabFn = (data: any) => {
activeName.value = data
storage.set({ key: 'storeActiveName', data })
@ -873,12 +888,6 @@ let notificationEl = null
const getInstallTask = (first: boolean = true) => {
getAddonInstalltask().then((res) => {
if (res.data) {
upgradeStartTime.value = Date.now()
upgradeDuration.value = 0
if (upgradeTimer) clearInterval(upgradeTimer)
upgradeTimer = setInterval(() => {
upgradeDuration.value++
}, 1000)
if (first) {
installLog = []
currAddon.value = res.data.addon
@ -894,9 +903,14 @@ const getInstallTask = (first: boolean = true) => {
showClose: false
})
}
if (upgradeTimer) clearInterval(upgradeTimer)
upgradeDuration.value = parseInt(Date.now() / 1000) - res.data.timestamp
upgradeTimer = setInterval(() => {
upgradeDuration.value++
}, 1000)
}
if (res.data.error) {
terminalRef.value.pushMessage({ content: res.data.error, class: 'error' })
terminalRef.value?.pushMessage({ content: res.data.error, class: 'error' })
errorMsg.value = res.data.error
errorDialog.value = true
if (upgradeTimer) {
@ -912,7 +926,6 @@ const getInstallTask = (first: boolean = true) => {
setTimeout(() => {
getInstallTask(false)
}, 2000)
} else {
if (!first) {
installStep.value = 2
@ -922,11 +935,12 @@ const getInstallTask = (first: boolean = true) => {
}
localListFn()
userStore.clearRouters()
notificationEl.close()
notificationEl?.close()
}
}
}).catch((e) => {
terminalRef.value.pushMessage({ content: e.message, class: 'error' })
console.log(e)
terminalRef.value?.pushMessage({ content: e.message, class: 'error' })
})
}
@ -1252,8 +1266,6 @@ const versionJudge = (row: any) => {
return false
}
const batchUpgradeApp = ref<String[]>([])
const appKeyAllSelect = () => {
if (localList.value[activeName.value].length) {
if (localList.value[activeName.value].length == batchUpgradeApp.value.length) {
@ -1583,6 +1595,11 @@ html.dark .table-head-bg {
.app-card {
width: calc((100% - 120px) / 5);
min-width: 260px;
&.selected {
background-color: var(--el-color-primary-light-9);
border-color: var(--el-color-primary-light-9);
}
}
</style>

View File

@ -33,8 +33,8 @@
<el-main class="login-main w-full mt-[120px] justify-center h-0" v-else-if="!imgLoading && loginType == 'site'">
<div>
<div class="login-main-left flex flex-col items-center justify-center">
<div class="w-[130px] h-[40px] overflow-hidden" v-if="webSite.site_login_logo">
<el-image class="w-full h-full" :src="img(webSite.site_login_logo)" fit="contain">
<div class="w-[130px] h-[40px] overflow-hidden" v-if="loginConfig.site_login_logo">
<el-image class="w-full h-full" :src="img(loginConfig.site_login_logo)" fit="contain">
<template #error>
<div class="flex justify-center items-center w-full h-full">
<img class="max-w-[130px]" src="@/app/assets/images/white_logo.png" alt="" object-fit="contain" />
@ -119,7 +119,7 @@ route.redirectedFrom && (route.query.redirect = route.redirectedFrom.path)
const webSite: any = computed(() => useSystemStore().website)
const siteBackgroundStyle = computed(() => ({
backgroundImage: webSite.value?.site_login_bg_img ? `url(${img(webSite.value.site_login_bg_img)})` : '',
backgroundImage: loginConfig.value?.site_login_bg_img ? `url(${img(loginConfig.value.site_login_bg_img)})` : '',
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center'

View File

@ -279,6 +279,8 @@ const previewPoster = (data: any) => {
previewDialogVisible.value = true
}
isRepeat.value = false
}).catch(() => {
isRepeat.value = false
})
}

View File

@ -23,6 +23,18 @@
<upload-image v-model="formData.site_bg" />
<div class="form-tip">{{t('siteBgImgTip')}}</div>
</el-form-item>
<el-form-item :label="t('siteLoginLogo')">
<div>
<upload-image v-model="formData.site_login_logo" />
<p class="text-[12px] text-[#a9a9a9]">{{ t('siteLoginLogoTips') }}</p>
</div>
</el-form-item>
<el-form-item :label="t('siteLoginBgImg')">
<div>
<upload-image v-model="formData.site_login_bg_img" />
<p class="text-[12px] text-[#a9a9a9]">{{ t('siteLoginBgImgTips') }}</p>
</div>
</el-form-item>
</div>
</el-card>
</el-form>
@ -51,7 +63,9 @@ const formData = reactive<Record<string, number | string>>({
is_captcha: 0,
is_site_captcha: 0,
bg: '',
site_bg: ''
site_bg: '',
site_login_logo: '',
site_login_bg_img: ''
})
const getFormData = async () => {

View File

@ -1,80 +1,80 @@
<template>
<div class="main-container">
<el-card class="box-card !border-none" shadow="never">
<div class="main-container">
<el-card class="box-card !border-none" shadow="never">
<div class="flex justify-between items-center">
<span class="text-page-title">{{ pageName }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-page-title">{{ pageName }}</span>
</div>
<div class="mt-[20px]">
<el-table :data="addonList" size="large" v-loading="loading">
<template #empty>
<span>{{ !loading ? t('emptyData') : '' }}</span>
</template>
<div class="mt-[20px]">
<el-table :data="addonList" size="large" v-loading="loading">
<template #empty>
<span>{{ !loading ? t('emptyData') : '' }}</span>
</template>
<el-table-column prop="title" :label="t('app')" min-width="120" >
<template #default="{ row }">
<div class="flex items-center">
<el-image class="w-[40px] h-[40px] rounded-md overflow-hidden" :src="row.icon" fit="contain">
<template #error>
<div class="flex items-center w-full h-full">
<img class="w-full h-full" src="@/app/assets/images/icon-addon-one.png" alt="">
</div>
</template>
</el-image>
<div class="flex-1 ml-2 truncate">{{ row.title }}</div>
</div>
</template>
</el-table-column>
<el-table-column :label="t('operation')" align="right" fixed="right" width="100">
<template #default="{ row }">
<el-button type="primary" link @click="openSettingLayer(row.key)">{{ t('setting') }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
<el-dialog v-model="showDialog" :title="t('selectLayout')" width="800" :destroy-on-close="true">
<div class="h-[300px]">
<el-scrollbar >
<div class="flex justify-between items-center mb-[20px]">
<h3 class="!text-sm !text-[#444]">{{ t('layout') }}</h3>
<div class="flex items-center cursor-pointer" @click="toDiyLayout">
<span class="iconfont iconwenhao text-[#999] !text-[14px]"></span>
<div class="ml-[2px] text-[12px] text-[#999]">如何开发自定义布局</div>
<el-table-column prop="title" :label="t('app')" min-width="120" >
<template #default="{ row }">
<div class="flex items-center">
<el-image class="w-[40px] h-[40px] rounded-md overflow-hidden" :src="row.icon" fit="contain">
<template #error>
<div class="flex items-center w-full h-full">
<img class="w-full h-full" src="@/app/assets/images/icon-addon-one.png" alt="">
</div>
</div>
<div class="flex justify-items-stretch">
<div class="w-[180px] h-[130px] mr-[10px] mb-[10px] border hover:border-primary cursor-pointer"
:class="{'border-primary': ((!layoutConfig[currAddon] && item.layout == 'default') || (layoutConfig[currAddon] == item.layout)) }"
@click="layoutConfig[currAddon] = item.layout"
v-for="item in systemLayout">
<img :src="img(item.cover)" class="w-full h-full" />
</div>
<div class="w-[180px] h-[130px] mr-[20px] border hover:border-primary cursor-pointer"
:class="{'border-primary': ((!layoutConfig[currAddon] && addonLayout.layout == 'default') || (layoutConfig[currAddon] == addonLayout.layout)) }"
@click="layoutConfig[currAddon] = addonLayout.layout"
v-if="addonLayout">
<img :src="img(addonLayout.cover)" class="w-full h-full" />
</div>
</div>
<!-- <h3 class="panel-title !text-sm">{{ t('themeColor') }}</h3>
<div>
<el-color-picker v-model="themeColor[currAddon]" size="large" />
<div class="form-tip text-[#999] mt-2">设置的色调会在前端站点列表体现[home/index]用于区分不同的应用</div>
</div> -->
</el-scrollbar>
</template>
</el-image>
<div class="flex-1 ml-2 truncate">{{ row.title }}</div>
</div>
</template>
</el-table-column>
<el-table-column :label="t('operation')" align="right" fixed="right" width="100">
<template #default="{ row }">
<el-button type="primary" link @click="openSettingLayer(row.key)">{{ t('setting') }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
<el-dialog v-model="showDialog" :title="t('selectLayout')" width="800" :destroy-on-close="true">
<div class="h-[300px]">
<el-scrollbar >
<div class="flex justify-between items-center mb-[20px]">
<h3 class="!text-sm !text-[#444]">{{ t('layout') }}</h3>
<div class="flex items-center cursor-pointer" @click="toDiyLayout">
<span class="iconfont iconwenhao text-[#999] !text-[14px]"></span>
<div class="ml-[2px] text-[12px] text-[#999]">如何开发自定义布局</div>
</div>
</div>
<template #footer>
<div class="flex justify-items-stretch">
<div class="w-[180px] h-[130px] mr-[10px] mb-[10px] border hover:border-primary cursor-pointer"
:class="{'border-primary': ((!layoutConfig[currAddon] && item.layout == 'default') || (layoutConfig[currAddon] == item.layout)) }"
@click="layoutConfig[currAddon] = item.layout"
v-for="item in systemLayout">
<img :src="img(item.cover)" class="w-full h-full" />
</div>
<div class="w-[180px] h-[130px] mr-[20px] border hover:border-primary cursor-pointer"
:class="{'border-primary': ((!layoutConfig[currAddon] && addonLayout.layout == 'default') || (layoutConfig[currAddon] == addonLayout.layout)) }"
@click="layoutConfig[currAddon] = addonLayout.layout"
v-if="addonLayout">
<img :src="img(addonLayout.cover)" class="w-full h-full" />
</div>
</div>
<!-- <h3 class="panel-title !text-sm">{{ t('themeColor') }}</h3>
<div>
<el-color-picker v-model="themeColor[currAddon]" size="large" />
<div class="form-tip text-[#999] mt-2">设置的色调会在前端站点列表体现[home/index]用于区分不同的应用</div>
</div> -->
</el-scrollbar>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
@ -93,67 +93,67 @@ const systemLayout = ref([])
const addonLayout = ref(null)
const themeColor = ref({})
const addonList = ref([
{
title: t('manyApp'),
key: 'system',
icon: ''
}
{
title: t('manyApp'),
key: 'system',
icon: ''
}
])
const layouts = import.meta.globEager('@/layout/**/layout.json')
for (const key in layouts) {
const layout = layouts[key]
systemLayout.value.push(layout.default)
const layout = layouts[key]
systemLayout.value.push(layout.default)
}
const addonLayouts = import.meta.globEager('@/addon/**/layout/layout.json')
getInstalledAddonList().then(({ data }) => {
Object.keys(data).forEach((key) => {
const item = data[key]
item.type == 'app' && addonList.value.push(item)
})
loading.value = false
Object.keys(data).forEach((key) => {
const item = data[key]
item.type == 'app' && addonList.value.push(item)
})
loading.value = false
})
const getLayoutConfig = () => {
getLayout().then(({ data }) => {
layoutConfig.value = data
})
getThemecolor().then(({ data }) => {
themeColor.value = data
})
getLayout().then(({ data }) => {
layoutConfig.value = data
})
getThemecolor().then(({ data }) => {
themeColor.value = data
})
}
getLayoutConfig()
const currAddon = ref('')
const showDialog = ref(false)
const openSettingLayer = async (key: string) => {
if (key != 'system') {
addonLayout.value = null
Object.keys(addonLayouts).forEach((path) => {
path.indexOf(`/addon/${key}/`) != -1 && (addonLayout.value = addonLayouts[path].default)
})
}
currAddon.value = key
showDialog.value = true
if (key != 'system') {
addonLayout.value = null
Object.keys(addonLayouts).forEach((path) => {
path.indexOf(`/addon/${key}/`) != -1 && (addonLayout.value = addonLayouts[path].default)
})
}
currAddon.value = key
showDialog.value = true
}
const confirm = () => {
setThemecolor({
key: currAddon.value,
value: themeColor.value[currAddon.value] ? themeColor.value[currAddon.value] : ''
})
setLayout({
key: currAddon.value,
value: layoutConfig.value[currAddon.value] ? layoutConfig.value[currAddon.value] : 'default'
})
showDialog.value = false
setThemecolor({
key: currAddon.value,
value: themeColor.value[currAddon.value] ? themeColor.value[currAddon.value] : ''
})
setLayout({
key: currAddon.value,
value: layoutConfig.value[currAddon.value] ? layoutConfig.value[currAddon.value] : 'default'
})
showDialog.value = false
}
//
const toDiyLayout = () => {
let url = 'https://doc.niucloud.com/saas.html?keywords=/ru-he-kai-fa-zi-ding-yi-bu-ju-hou-tai-bu-ju';
window.open(url)
let url = 'https://doc.niucloud.com/saas.html?keywords=/pluginDev/diyLayoutDev';
window.open(url)
}
</script>

View File

@ -20,18 +20,6 @@
<p class="text-[12px] text-[#a9a9a9]">{{ t('iconPlaceholder') }}</p>
</div>
</el-form-item>
<el-form-item :label="t('siteLoginLogo')" v-if="appType == 'admin'">
<div>
<upload-image v-model="formData.site_login_logo" />
<p class="text-[12px] text-[#a9a9a9]">{{ t('siteLoginLogoTips') }}</p>
</div>
</el-form-item>
<el-form-item :label="t('siteLoginBgImg')" v-if="appType == 'admin'">
<div>
<upload-image v-model="formData.site_login_bg_img" />
<p class="text-[12px] text-[#a9a9a9]">{{ t('siteLoginBgImgTips') }}</p>
</div>
</el-form-item>
<el-form-item :label="t('keywords')">
<el-input v-model.trim="formData.keywords" :placeholder="t('keywordsPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
</el-form-item>

View File

@ -36,14 +36,14 @@
<el-form-item prop="create_time" v-if="isShow">
<el-date-picker v-model="siteTableData.searchParam.create_time" type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss" :start-placeholder="t('startDate')"
:end-placeholder="t('endDate')" />
value-format="YYYY-MM-DD HH:mm:ss" :start-placeholder="t('createTimeStartDate')"
:end-placeholder="t('createTimeEndDate')" />
</el-form-item>
<el-form-item prop="expire_time" v-if="isShow">
<el-date-picker v-model="siteTableData.searchParam.expire_time" type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss" :start-placeholder="t('startDate')"
:end-placeholder="t('endDate')" />
value-format="YYYY-MM-DD HH:mm:ss" :start-placeholder="t('expireTimeStartDate')"
:end-placeholder="t('expireTimeEndDate')" />
</el-form-item>
<el-form-item>
@ -53,7 +53,7 @@
</el-form-item>
</el-form>
<div class="flex justify-end items-center flex-1">
<div class="right-btn-group ">
<div class="right-btn-group ">
<tempalte class="flex items-center">
<el-tooltip
class="box-item"
@ -392,7 +392,7 @@ const getSiteAllowChangeFn = ()=>{
let isAllowChange = allowChange.value ? true : false
localStorage.setItem('isAllowChange',isAllowChange.toString())
})
}
getSiteAllowChangeFn()

View File

@ -44,7 +44,7 @@ const type = ref(attachmentType[0])
.el-tabs {
display: flex;
flex-direction: column;
flex-direction: column-reverse;
height: calc(100% - 40px);
}

View File

@ -53,7 +53,7 @@
<el-table-column :label="t('operation')" align="right" fixed="right" width="330">
<template #default="{ row }">
<div class="flex flex-col items-end">
<el-button type="primary" link @click="commit(row.group_id)">{{ t('weappVersionUpdate') }}</el-button>
<el-button type="primary" link :loading="uploading == row.group_id" @click="commit(row.group_id)">{{ t('weappVersionUpdate') }}</el-button>
<el-button type="primary" link @click="showCommitRecordDialog(row.group_id)">{{ t('weappVersionUpdateRecord') }}</el-button>
<el-button type="primary" link @click="syncSiteWeappFn(row.group_id)">{{ t('siteWeappSync') }}</el-button>
</div>
@ -82,6 +82,7 @@
<el-table-column prop="create_time" :label="t('createTime')"></el-table-column>
<el-table-column :label="t('operation')" align="right" fixed="right" width="130">
<template #default="{ row }">
<el-button type="primary" link v-if="row.status == 0 || row.status == -1" @click="deleteVersion(row)">{{ t('delete') }}</el-button>
<el-button type="primary" link v-if="row.status == -1" @click="handleFailReason(row)">{{ t('failReason') }}</el-button>
</template>
</el-table-column>
@ -96,19 +97,27 @@
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
{{ failReason }}
</el-scrollbar>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="helpInfo">{{ t('helpInfo') }}</el-button>
<el-button @click="failReasonDialogVisible = false">
{{ t('close') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { useRoute } from 'vue-router'
import { getWeappLastCommitRecord, weappCommit, getWeappCommitRecord, getSiteGroupCommitRecord, syncSiteWeapp } from '@/app/api/wxoplatform'
import { getWeappLastCommitRecord, weappCommit, getWeappCommitRecord, getSiteGroupCommitRecord, syncSiteWeapp, delVersion } from '@/app/api/wxoplatform'
import { t } from '@/lang'
import { ElMessageBox } from 'element-plus'
const route = useRoute()
const pageName = route.meta.title
const uploading = ref(false)
const uploading = ref(0)
const lastRecord = ref({})
const commitRecordDialogShow = ref(false)
const commitRecordTableData = reactive({
@ -136,19 +145,36 @@ const handleFailReason = (data: any) => {
failReason.value = data.fail_reason
failReasonDialogShow.value = true
}
const helpInfo = () => {
window.open('https://doc.niucloud.com/saasUse.html?keywords=/configFAQ/minWaChatUpload')
}
const deleteVersion = (data: any) => {
ElMessageBox.confirm(t('delWeappVersionTips'), t('warning'),
{
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}
).then(() => {
delVersion(data.id).then(() => {
loadCommitRecordList()
})
})
}
getWeappLastCommitRecord().then(({ data }) => {
lastRecord.value = data
})
const commit = (siteGroupId = '') => {
if (uploading.value) return
uploading.value = true
if (uploading.value != 0) return
uploading.value = siteGroupId
weappCommit({ site_group_id: siteGroupId }).then(() => {
uploading.value = false
uploading.value = 0
}).catch(() => {
uploading.value = false
uploading.value = 0
})
}

View File

@ -60,11 +60,11 @@
<el-input v-model="attachmentParam.real_name" class="m-0 !w-[200px]" clearable :placeholder="t('upload.placeholder' + type + 'Name')" prefix-icon="Search" @input="getAttachmentList()" />
</el-col>
</el-row>
<div class="flex-1 my-[15px] h-0" v-loading="attachment.loading">
<div class="flex-1 mb-[15px] mt-[10px] h-0" v-loading="attachment.loading">
<el-scrollbar>
<!-- 选择弹出框 -->
<div class="flex flex-wrap" v-if="attachment.data.length && (operate === true || scene != 'attachment')">
<div class="attachment-item mr-[10px]" :class="scene == 'select' ? 'w-[100px]' : 'w-[120px]'" v-for="(item, index) in attachment.data" :key="index">
<div class="attachment-item relative mr-[10px] mt-[6px]" :class="scene == 'select' ? 'w-[100px]' : 'w-[120px]'" v-for="(item, index) in attachment.data" :key="index">
<div class="attachment-wrap w-full rounded cursor-pointer overflow-hidden relative flex items-center justify-center" :class="scene == 'select' ? 'h-[100px]' : 'h-[120px]'" @click="selectFile(item)">
<el-image :src="img(item.thumb)" fit="contain" v-if="type == 'image'"/>
@ -76,9 +76,10 @@
<span class="absolute bottom-[2px] right-[2px] text-white z-[2] leading-none">{{ getFileIndex(item.att_id) }}</span>
</div>
</div>
</div>
<icon name="element CircleCloseFilled" v-if="type != 'icon' && !operate" @click="deleteAttachmentEvent(index)" class="top-[-6px] right-[-6px] absolute z-[3] del-icon hidden cursor-pointer opacity-[0.8]" />
<div class="flex items-center">
<el-tooltip placement="top">
<template #content>{{ item.real_name }}</template>
@ -110,7 +111,7 @@
<!-- 素材管理 -->
<div class="flex flex-wrap" v-else-if="attachment.data.length && operate === false">
<div class="attachment-item mr-[10px] w-[120px]" v-for="(item, index) in attachment.data" :key="index">
<div class="attachment-item relative mr-[10px] mt-[6px] w-[120px]" v-for="(item, index) in attachment.data" :key="index">
<div class="attachment-wrap w-full rounded cursor-pointer overflow-hidden relative flex items-center justify-center h-[120px]">
<el-image :src="img(item.thumb)" fit="contain" v-if="type == 'image'" :preview-src-list="item.image_list"/>
<video :src="img(item.thumb)" v-else-if="type == 'video'" @click="previewVideo(index)"></video>
@ -122,6 +123,7 @@
<div class="truncate my-[10px] cursor-pointer text-base flex-1 text-center">{{ item.real_name }}</div>
</el-tooltip>
</div>
<icon name="element CircleCloseFilled" v-if="type != 'icon'" @click="deleteAttachmentEvent(index)" class="top-[-6px] right-[-6px] absolute z-[3] del-icon hidden cursor-pointer opacity-[0.8]" />
</div>
</div>
<div class="flex absolute top-0 left-0 right-0 bottom-0 items-center justify-center" v-else>
@ -669,6 +671,9 @@ defineExpose({
.attachment-action {
display: block;
}
.del-icon {
display: block;
}
}
.attachment-list-wrap {

View File

@ -79,7 +79,7 @@ routers.forEach(item => {
const oneMenuActive = ref(oneMenuData.value[0].name)
watch(route, () => {
twoMenuData.value = route.matched[1].children ?? []
twoMenuData.value = route.matched[2].children ?? []
oneMenuActive.value = route.matched[1].name == ADMIN_ROUTE.children[0].name ? route.matched[2].name : route.matched[1].name
defaultOpeneds.value = twoMenuData.value.map(item => item.name)
}, { immediate: true })

View File

@ -15,7 +15,7 @@
<el-scrollbar>
<div class="p-[15px]">
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
<keep-alive :include="tabbarStore.tabNames">
<keep-alive :include="tabbarStore.tabNames" :max="15">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
@ -25,7 +25,7 @@
<!-- 主体 end -->
</el-container>
</el-container>
</div>
</div>
</template>
<script lang="ts" setup>

View File

@ -15,7 +15,7 @@
<el-scrollbar>
<div class="p-[15px]">
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
<keep-alive :include="tabbarStore.tabNames">
<keep-alive :include="tabbarStore.tabNames" :max="15">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
@ -25,7 +25,7 @@
<!-- 主体 end -->
</el-container>
</el-container>
</div>
</div>
</template>
<script lang="ts" setup>

View File

@ -33,7 +33,7 @@
</el-scrollbar>
</div>
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[190px]">
<div class="w-[190px] h-[64px] flex items-center justify-center text-[16px] border-0 border-b-[1px] border-solid border-[#eee]">{{ route.matched[1].meta.title }}</div>
<div class="w-[190px] h-[64px] flex items-center justify-center text-[16px] border-0 border-b-[1px] border-solid border-[#eee]">{{ route.matched[2].meta.title }}</div>
<el-menu :default-active="route.name" :router="true" class="aside-menu" :collapse="systemStore.menuIsCollapse">
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
</el-menu>
@ -307,21 +307,21 @@ watch(route, () => {
} else {
//
if (siteInfo?.apps.length > 1) {
twoMenuData.value = route.matched[1].children
oneMenuActive.value = route.matched[1].name
twoMenuData.value = route.matched[2].children
oneMenuActive.value = route.matched[2].name
} else {
//
const oneMenu = route.matched[1]
const oneMenu = route.matched[2]
if (oneMenu.meta.addon == '') {
oneMenuActive.value = route.matched[1].name
twoMenuData.value = route.matched[1].children ?? []
oneMenuActive.value = route.matched[2].name
twoMenuData.value = route.matched[2].children ?? []
} else {
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
oneMenuActive.value = route.matched[3].name
twoMenuData.value = route.matched[3].children ?? []
} else {
oneMenuActive.value = route.matched[2].name
twoMenuData.value = route.matched[2].children ?? []
} else {
oneMenuActive.value = route.matched[1].name
twoMenuData.value = route.matched[1].children ?? []
}
}
}

View File

@ -18,7 +18,7 @@
<el-scrollbar>
<div class="p-[15px]">
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
<keep-alive :include="tabbarStore.tabNames">
<keep-alive :include="tabbarStore.tabNames" :max="15">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>

View File

@ -18,7 +18,7 @@
<el-scrollbar>
<div class="p-[15px]">
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
<keep-alive :include="tabbarStore.tabNames">
<keep-alive :include="tabbarStore.tabNames" :max="15">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>

View File

@ -18,7 +18,7 @@
<el-scrollbar>
<div class="p-[15px]">
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
<keep-alive :include="tabbarStore.tabNames">
<keep-alive :include="tabbarStore.tabNames" :max="15">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>

View File

@ -40,7 +40,7 @@
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[152px]">
<div class="w-[152px] h-[64px] flex items-center justify-center text-[16px] border-b-[1px] border-solid border-[var(--el-border-color-lighter)]">
{{ route.matched[1].meta.title }}
{{ route.matched[2].meta.title }}
</div>
<el-menu class="aside-menu" :default-active="route.name" :default-openeds="menuOption" :router="true" :collapse="systemStore.menuIsCollapse">
@ -327,21 +327,21 @@ watch(route, () => {
} else {
//
if (siteInfo?.apps.length > 1) {
twoMenuData.value = route.matched[1].children
oneMenuActive.value = route.matched[1].name
twoMenuData.value = route.matched[2].children
oneMenuActive.value = route.matched[2].name
} else {
//
const oneMenu = route.matched[1]
const oneMenu = route.matched[2]
if (oneMenu.meta.addon == '') {
oneMenuActive.value = route.matched[1].name
twoMenuData.value = route.matched[1].children ?? []
oneMenuActive.value = route.matched[2].name
twoMenuData.value = route.matched[2].children ?? []
} else {
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
oneMenuActive.value = route.matched[3].name
twoMenuData.value = route.matched[3].children ?? []
} else {
oneMenuActive.value = route.matched[2].name
twoMenuData.value = route.matched[2].children ?? []
} else {
oneMenuActive.value = route.matched[1].name
twoMenuData.value = route.matched[1].children ?? []
}
}
}

View File

@ -18,7 +18,7 @@
<el-scrollbar>
<div class="p-[15px]">
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
<keep-alive :include="tabbarStore.tabNames">
<keep-alive :include="tabbarStore.tabNames" :max="15">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>

View File

@ -134,20 +134,11 @@ router.beforeEach(async (to: any, from, next) => {
// 添加动态路由
userStore.routers.forEach(route => {
if (!route.children) {
if (route.meta.app == 'admin') {
router.addRoute(ADMIN_ROUTE.children[0].name, route)
} else {
router.addRoute(SITE_ROUTE.children[0].name, route)
}
return
}
// 动态添加可访问路由表
if (route.meta.app == 'admin') {
router.addRoute(ADMIN_ROUTE.name, route)
router.addRoute(ADMIN_ROUTE.children[0].name, route)
} else {
router.addRoute(SITE_ROUTE.name, route)
router.addRoute(SITE_ROUTE.children[0].name, route)
}
})
next(to)

View File

@ -25,12 +25,13 @@ export const ROOT_ROUTER: RouteRecordRaw = {
// 平台端根路由
export const ADMIN_ROUTE: RouteRecordRaw = {
path: '/admin',
name: Symbol('admin'),
name: 'admin',
children: [
{
path: '',
name: Symbol('adminRoot'),
component: Default
name: 'adminRoot',
component: Default,
children: []
},
{
path: 'login',
@ -46,16 +47,16 @@ export const ADMIN_ROUTE: RouteRecordRaw = {
// HOME端根路由
export const HOME_ROUTE: RouteRecordRaw = {
path: '/home',
name: Symbol('home'),
name: 'home',
children: [
{
path: '',
name: Symbol('homeRoot'),
name: 'homeRoot',
component: Default
},
{
path: 'index',
name: Symbol('homeIndex'),
name: 'homeIndex',
meta: {
type: 1,
title: '站点管理'
@ -68,12 +69,13 @@ export const HOME_ROUTE: RouteRecordRaw = {
// 站点端根路由
export const SITE_ROUTE: RouteRecordRaw = {
path: '/site',
name: Symbol('site'),
name: 'site',
children: [
{
path: '',
name: Symbol('siteRoot'),
component: Default
name: 'siteRoot',
component: Default,
children: []
},
{
path: 'wxoplatform/callback',
@ -98,7 +100,7 @@ export const SITE_ROUTE: RouteRecordRaw = {
export const DECORATE_ROUTER: RouteRecordRaw = {
path: '/decorate',
component: Decorate,
name: Symbol('decorate'),
name: 'decorate',
children: []
}
@ -144,12 +146,11 @@ const createRoute = function (route: Route, parentRoute: RouteRecordRaw | null =
addon: route.addon,
attr: route.menu_attr,
parent_route: parentRoute ? parentRoute.meta : parentRoute,
sort: route.sort
sort: route.sort,
componentName: route.menu_key
}
}
if (route.menu_type == 0) {
record.component = parentRoute ? RouterView : () => Promise.resolve(Default)
} else {
if (route.menu_type == 1) {
record.component = route.addon ? addonModules[`/src/addon/${ route.addon }/views/${ route.view_path }.vue`] : modules[`/src/app/views/${ route.view_path }.vue`]
}
return record

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import type { RouteLocationNormalizedLoaded, RouteRecordName } from 'vue-router'
import useSystemStore from '@/stores/modules/system'
interface Tabbar {
curr: string,
@ -26,7 +27,8 @@ const useTabbarStore = defineStore('tabbar', {
path: roter.path,
title: roter.meta ? roter.meta.title : '',
name: roter.name,
query: roter.query || {}
query: roter.query || {},
compomentName: roter.matched.at(-1).components.default.__name
}
},
removeTab(path: string) {
@ -40,8 +42,9 @@ const useTabbarStore = defineStore('tabbar', {
tabLength: (state) => Object.keys(state.tabs).length,
tabNames: (state) => {
const name: any[] = []
if (!useSystemStore().tab) return name
Object.keys(state.tabs).forEach(key => {
name.push(state.tabs[key].name)
name.push(state.tabs[key].compomentName)
})
return name
}

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "nc-iconfont"; /* Project id 4567203 */
src: url('//at.alicdn.com/t/c/font_4567203_9qmfc2elgrt.woff2?t=1741345195504') format('woff2'),
url('//at.alicdn.com/t/c/font_4567203_9qmfc2elgrt.woff?t=1741345195504') format('woff'),
url('//at.alicdn.com/t/c/font_4567203_9qmfc2elgrt.ttf?t=1741345195504') format('truetype');
src: url('//at.alicdn.com/t/c/font_4567203_mq3zv60st5a.woff2?t=1767940264104') format('woff2'),
url('//at.alicdn.com/t/c/font_4567203_mq3zv60st5a.woff?t=1767940264104') format('woff'),
url('//at.alicdn.com/t/c/font_4567203_mq3zv60st5a.ttf?t=1767940264104') format('truetype');
}
.nc-iconfont {
@ -13,6 +13,578 @@
-moz-osx-font-smoothing: grayscale;
}
.nc-icon-huiyuangaikuang:before {
content: "\e91b";
}
.nc-icon-jiaoyigaikuang:before {
content: "\e91a";
}
.nc-icon-zhanghuruzhushenqing:before {
content: "\e919";
}
.nc-icon-jiesuanzhanghu1:before {
content: "\e918";
}
.nc-icon-zhanghuguanli:before {
content: "\e917";
}
.nc-icon-banbenhao:before {
content: "\e8ff";
}
.nc-icon-shangjiashoujiduan:before {
content: "\e8fd";
}
.nc-icon-xinxi:before {
content: "\e900";
}
.nc-icon-a-gongyingshangzhangdanpc30:before {
content: "\e771";
}
.nc-icon-a-jifenshangchengzhangdanpc30:before {
content: "\e772";
}
.nc-icon-a-shangchengzhangdanpc30:before {
content: "\e773";
}
.nc-icon-a-chongzhizhangdanpc30:before {
content: "\e774";
}
.nc-icon-gongzuotai:before {
content: "\e8ec";
}
.nc-icon-shoujiV6xx1:before {
content: "\e8ed";
}
.nc-icon-shoujiV6xxmm:before {
content: "\e8fe";
}
.nc-icon-xuanfuanniu:before {
content: "\e8e5";
}
.nc-icon-pintuanguanli:before {
content: "\e8e0";
}
.nc-icon-huodongliebiao1:before {
content: "\e8e1";
}
.nc-icon-pintuanliebiao:before {
content: "\e8e2";
}
.nc-icon-pintuandingdan:before {
content: "\e8e3";
}
.nc-icon-pintuanpeizhi:before {
content: "\e8e4";
}
.nc-icon-xia:before {
content: "\e8de";
}
.nc-icon-shang:before {
content: "\e8df";
}
.nc-icon-shoujiV6xx:before {
content: "\e7a1";
}
.nc-icon-ditumichi:before {
content: "\e8ca";
}
.nc-icon-qiehuanV6xx:before {
content: "\e8eb";
}
.nc-icon-dingdanbianhaoV6xx:before {
content: "\e8ea";
}
.nc-icon-dingdanbianhaoV6mm:before {
content: "\e8e9";
}
.nc-icon-dianpuzhandianV6xx2:before {
content: "\e8e6";
}
.nc-icon-huocheV6mm:before {
content: "\e8e7";
}
.nc-icon-huocheV6xx:before {
content: "\e8e8";
}
.nc-icon-wannengbiaodanV6xx:before {
content: "\e8d9";
}
.nc-icon-shujudaochuV6xx:before {
content: "\e8da";
}
.nc-icon-shujudaochuV6xx-1:before {
content: "\e8db";
}
.nc-icon-dayinjiV6xx:before {
content: "\e8dc";
}
.nc-icon-dayinV6xx:before {
content: "\e8dd";
}
.nc-icon-fenzuguanliV6xx:before {
content: "\e8d6";
}
.nc-icon-shangpinliebiaoV6xx:before {
content: "\e8d7";
}
.nc-icon-dingdanliebiaoV6xx:before {
content: "\e8d4";
}
.nc-icon-tuikuanV6xx2:before {
content: "\e8d8";
}
.nc-icon-jifenshangchengV6xx:before {
content: "\e8d5";
}
.nc-icon-chakandituV6xx:before {
content: "\e8d2";
}
.nc-icon-a-peisongguanliPC30:before {
content: "\e6ed";
}
.nc-icon-a-tongchengpeisongPC30:before {
content: "\e6ec";
}
.nc-icon-a-wuliupeisongPC30:before {
content: "\e6ee";
}
.nc-icon-a-shangjiapeisongPC30:before {
content: "\e6f2";
}
.nc-icon-xianshiV6mm:before {
content: "\e8ac";
}
.nc-icon-a-dianneifenrun34:before {
content: "\e811";
}
.nc-icon-xiaochengxu:before {
content: "\e8a8";
}
.nc-icon-qianV6xx:before {
content: "\e784";
}
.nc-icon-mimaV6xx:before {
content: "\e8a6";
}
.nc-icon-yanzhengmaV6xx:before {
content: "\e8a7";
}
.nc-icon-bianjiV6xx2:before {
content: "\e8a4";
}
.nc-icon-fenxiangV6mm-1:before {
content: "\e89c";
}
.nc-icon-a-Group1036V6xx:before {
content: "\e77b";
}
.nc-icon-shanchu-yuangaiV6xx:before {
content: "\e6eb";
}
.nc-icon-hezuogonghuoshangV6xx:before {
content: "\e88b";
}
.nc-icon-hezuodianpu:before {
content: "\e85b";
}
.nc-icon-gongyingshangxitong:before {
content: "\e852";
}
.nc-icon-shangchengxitong:before {
content: "\e858";
}
.nc-icon-fenzhangshezhi:before {
content: "\e843";
}
.nc-icon-xuanpinpingtai:before {
content: "\e845";
}
.nc-icon-a-Maskgroup:before {
content: "\e840";
}
.nc-icon-fenzhangguanli:before {
content: "\e84b";
}
.nc-icon-gongyingshangdingdan:before {
content: "\e84d";
}
.nc-icon-gongyingshangshangpin:before {
content: "\e84e";
}
.nc-icon-shuaxinV6mm2:before {
content: "\e889";
}
.nc-icon-yitiaohuo:before {
content: "\e888";
}
.nc-icon-shangchengdingdan:before {
content: "\e837";
}
.nc-icon-shangchengshangpin:before {
content: "\e83f";
}
.nc-icon-gouwudaiV6xx1:before {
content: "\e886";
}
.nc-icon-dianpuV6xx1:before {
content: "\e880";
}
.nc-icon-youhuiquanV6xx2:before {
content: "\e87a";
}
.nc-icon-yinhangka2:before {
content: "\e83e";
}
.nc-icon-huiyuantixian:before {
content: "\e839";
}
.nc-icon-a-Financing-tworongzi2:before {
content: "\e825";
}
.nc-icon-a-Weixin-marketweixindianshang:before {
content: "\e821";
}
.nc-icon-a-Repairbianji:before {
content: "\e818";
}
.nc-icon-a-Diskcipan:before {
content: "\e81c";
}
.nc-icon-a-Historylishijilu:before {
content: "\e81b";
}
.nc-icon-a-Deliveryjiaohuo:before {
content: "\e81a";
}
.nc-icon-dianpuV6xx:before {
content: "\e871";
}
.nc-icon-Frame:before {
content: "\e816";
}
.nc-icon-zhifujianshu1:before {
content: "\e815";
}
.nc-icon-jiagoushuliang:before {
content: "\e814";
}
.nc-icon-xiadanshu:before {
content: "\e813";
}
.nc-icon-shangpinliulanliang:before {
content: "\e812";
}
.nc-icon-fenxiaodengji:before {
content: "\e807";
}
.nc-icon-fenxiaoguanli:before {
content: "\e809";
}
.nc-icon-dianzimiandan:before {
content: "\e806";
}
.nc-icon-fenxiaodingdan:before {
content: "\e804";
}
.nc-icon-fenxiaoshang:before {
content: "\e808";
}
.nc-icon-hexiaoyuan:before {
content: "\e805";
}
.nc-icon-shangpintongji:before {
content: "\e80a";
}
.nc-icon-fenxiaogaikuang:before {
content: "\e802";
}
.nc-icon-fenxiaoshangpin:before {
content: "\e801";
}
.nc-icon-yingyongliebiao:before {
content: "\e80b";
}
.nc-icon-huodongliebiao:before {
content: "\e80c";
}
.nc-icon-hexiaojilu:before {
content: "\e80d";
}
.nc-icon-piliangfahuo:before {
content: "\e80e";
}
.nc-icon-fenxiaoshezhi:before {
content: "\e80f";
}
.nc-icon-dianpu:before {
content: "\e800";
}
.nc-icon-caozuorizhi1:before {
content: "\e7ef";
}
.nc-icon-dingdanliebiao:before {
content: "\e7f0";
}
.nc-icon-hexiaoguanli:before {
content: "\e7ee";
}
.nc-icon-fapiaoguanli:before {
content: "\e7ed";
}
.nc-icon-dingdanshezhi:before {
content: "\e7ea";
}
.nc-icon-huiyuanbiaoqian:before {
content: "\e7eb";
}
.nc-icon-huishouzhan:before {
content: "\e7db";
}
.nc-icon-jiesuanzhanghu:before {
content: "\e7f1";
}
.nc-icon-guanliyuan1:before {
content: "\e7e6";
}
.nc-icon-huiyuanliebiao:before {
content: "\e7da";
}
.nc-icon-shangpinfenlei:before {
content: "\e7dd";
}
.nc-icon-jiaoseguanli:before {
content: "\e7e3";
}
.nc-icon-shangpinliebiao:before {
content: "\e7e8";
}
.nc-icon-shangpinpingjia:before {
content: "\e7e9";
}
.nc-icon-dianpushezhi:before {
content: "\e7e7";
}
.nc-icon-sucaiguanli1:before {
content: "\e7f2";
}
.nc-icon-tixianjilu:before {
content: "\e7e4";
}
.nc-icon-xiaopiaodayin:before {
content: "\e7f3";
}
.nc-icon-shangjiadizhiku:before {
content: "\e7e1";
}
.nc-icon-shujudaochu:before {
content: "\e7de";
}
.nc-icon-shangpincanshu:before {
content: "\e7e0";
}
.nc-icon-tuikuanweiquan:before {
content: "\e7f4";
}
.nc-icon-weiyemian:before {
content: "\e7f5";
}
.nc-icon-shouyezhuangxiu:before {
content: "\e7d9";
}
.nc-icon-xianshizhekou:before {
content: "\e7d8";
}
.nc-icon-youhuiquan:before {
content: "\e7d7";
}
.nc-icon-zichangaikuang:before {
content: "\e7d6";
}
.nc-icon-yunfeimoban:before {
content: "\e7f6";
}
.nc-icon-daifukuandingdan:before {
content: "\e7fa";
}
.nc-icon-cangkushangpinshuliang:before {
content: "\e7f9";
}
.nc-icon-daishouhuodingdan:before {
content: "\e7f8";
}
.nc-icon-tuikuan:before {
content: "\e7f7";
}
.nc-icon-tuikuandingdan:before {
content: "\e7fb";
}
.nc-icon-chushoushangpinshuliang:before {
content: "\e7fc";
}
.nc-icon-daifahuodingdan:before {
content: "\e7fd";
}
.nc-icon-dingdanliang:before {
content: "\e7fe";
}
.nc-icon-xiaoshoue:before {
content: "\e7ff";
}
.nc-icon-paihangbangV6xx1:before {
content: "\e7e2";
}
.nc-icon-bangdanguanliV6xx:before {
content: "\e86d";
}
.nc-icon-bangdanshezhiV6xx:before {
content: "\e86e";
}
.nc-icon-zhuanzhangV6xx-1:before {
content: "\e86a";
}
.nc-icon-qingliV6xx:before {
content: "\e865";
}

View File

@ -401,3 +401,12 @@ export function getTablePageStorage(where: any = {}) {
}
return data;
}
// 距离显示
export function distance(distance: string | number): string {
const dist = typeof distance === 'string' ? parseFloat(distance) : distance;
if (isNaN(dist)) return distance.toString();
return dist < 1 ? parseInt((dist * 1000).toString()) + 'm' : dist.toFixed(1) + 'km'
}

View File

@ -134,6 +134,9 @@ export const createPolygon = (map: any, geometriesData: any) => {
* @param key
*/
export const deleteGeometry = (key: string) => {
if (!geometry[key]) {
return
}
geometry[key].graphical.remove(key)
geometry[key].editor.delete()
}