mirror of
https://github.com/dataease/dataease.git
synced 2025-02-24 19:42:56 +08:00
Merge pull request #8056 from dataease/pr@dev-v2_dzz_mobile
Pr@dev v2 dzz mobile
This commit is contained in:
commit
3c113c4673
13
core/core-frontend/mobile.html
Normal file
13
core/core-frontend/mobile.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DataEase</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/pages/mobile/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
@ -42,6 +42,7 @@
|
||||
"qs": "^6.11.0",
|
||||
"snowflake-id": "^1.1.0",
|
||||
"tinymce": "^5.8.2",
|
||||
"vant": "^4.8.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-codemirror": "^6.1.1",
|
||||
|
@ -32,7 +32,7 @@ export function uploadFileResult(file, callback) {
|
||||
const fileUrl = staticResourcePath + newFileName
|
||||
const param = new FormData()
|
||||
param.append('file', file)
|
||||
uploadFile(fileId, param).then(() => {
|
||||
return uploadFile(fileId, param).then(() => {
|
||||
callback(fileUrl)
|
||||
})
|
||||
}
|
||||
|
BIN
core/core-frontend/src/assets/img/user.png
Normal file
BIN
core/core-frontend/src/assets/img/user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
core/core-frontend/src/assets/logo-bg.jpg
Normal file
BIN
core/core-frontend/src/assets/logo-bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 561 KiB |
@ -182,6 +182,10 @@ const multiplexingCanvasOpen = () => {
|
||||
multiplexingRef.value.dialogInit()
|
||||
}
|
||||
|
||||
const mobileConfig = () => {
|
||||
useEmitt().emitter.emit('mobileConfig')
|
||||
}
|
||||
|
||||
eventBus.on('preview', previewInner)
|
||||
eventBus.on('save', saveCanvasWithCheck)
|
||||
eventBus.on('clearCanvas', clearCanvas)
|
||||
@ -417,6 +421,12 @@ onMounted(() => {
|
||||
is-label
|
||||
@customClick="multiplexingCanvasOpen"
|
||||
></component-button-label>
|
||||
<component-button-label
|
||||
icon-name="icon_copy_filled"
|
||||
title="移动端"
|
||||
is-label
|
||||
@customClick="mobileConfig"
|
||||
></component-button-label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -13,7 +13,7 @@ import { isMainCanvas } from '@/utils/canvasUtils'
|
||||
import { activeWatermark } from '@/components/watermark/watermark'
|
||||
import { personInfoApi } from '@/api/user'
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const { pcMatrixCount, curComponent } = storeToRefs(dvMainStore)
|
||||
const { pcMatrixCount, curComponent, mobileInPc } = storeToRefs(dvMainStore)
|
||||
|
||||
const props = defineProps({
|
||||
canvasStyleData: {
|
||||
@ -274,7 +274,7 @@ defineExpose({
|
||||
:style="getShapeItemShowStyle(item)"
|
||||
:show-position="showPosition"
|
||||
:search-count="searchCount"
|
||||
:scale="scaleWidth"
|
||||
:scale="mobileInPc ? scaleWidth * 3 : scaleWidth"
|
||||
@userViewEnlargeOpen="userViewEnlargeOpen($event, item)"
|
||||
/>
|
||||
<user-view-enlarge ref="userViewEnlargeRef"></user-view-enlarge>
|
||||
|
@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<div class="shape" ref="shapeInnerRef" :id="domId" @dblclick="handleDbClick">
|
||||
<div v-if="showCheck" class="del-from-mobile" @click="delFromMobile">
|
||||
<label class="el-checkbox el-checkbox--small is-checked"
|
||||
><span class="el-checkbox__input is-checked"
|
||||
><input class="el-checkbox__original" checked type="checkbox" /><span
|
||||
class="el-checkbox__inner"
|
||||
></span></span
|
||||
></label>
|
||||
</div>
|
||||
<div
|
||||
class="shape-outer"
|
||||
v-show="contentDisplay"
|
||||
@ -113,7 +121,8 @@ const {
|
||||
curLinkageView,
|
||||
tabCollisionActiveId,
|
||||
tabMoveInActiveId,
|
||||
tabMoveOutComponentId
|
||||
tabMoveOutComponentId,
|
||||
mobileInPc
|
||||
} = storeToRefs(dvMainStore)
|
||||
const { editorMap, areaData, isCtrlOrCmdDown } = storeToRefs(composeStore)
|
||||
const emit = defineEmits([
|
||||
@ -126,6 +135,7 @@ const emit = defineEmits([
|
||||
'linkJumpSetOpen',
|
||||
'linkageSetOpen'
|
||||
])
|
||||
|
||||
const isEditMode = computed(() => editMode.value === 'edit')
|
||||
const state = reactive({
|
||||
seriesIdMap: {
|
||||
@ -247,6 +257,17 @@ const initialAngle = {
|
||||
}
|
||||
const cursors = ref({})
|
||||
|
||||
const showCheck = computed(() => {
|
||||
return mobileInPc.value && element.value.canvasId === 'canvas-main'
|
||||
})
|
||||
|
||||
const delFromMobile = () => {
|
||||
useEmitt().emitter.emit('onMobileStatusChange', {
|
||||
type: 'delFromMobile',
|
||||
value: element.value.id
|
||||
})
|
||||
}
|
||||
|
||||
const angleToCursor = [
|
||||
// 每个范围的角度对应的光标
|
||||
{ start: 338, end: 23, cursor: 'nw' },
|
||||
@ -902,6 +923,13 @@ onMounted(() => {
|
||||
<style lang="less" scoped>
|
||||
.shape {
|
||||
position: absolute;
|
||||
|
||||
.del-from-mobile {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.shape-shadow {
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="bar-main"
|
||||
v-if="!mobileInPc"
|
||||
:class="[
|
||||
showEditPosition,
|
||||
{
|
||||
@ -249,7 +250,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const { element, index, showPosition, canvasId } = toRefs(props)
|
||||
const { batchOptStatus, pcMatrixCount, curComponent, componentData, canvasViewInfo } =
|
||||
const { batchOptStatus, pcMatrixCount, curComponent, componentData, canvasViewInfo, mobileInPc } =
|
||||
storeToRefs(dvMainStore)
|
||||
|
||||
const state = reactive({
|
||||
|
@ -119,7 +119,7 @@ import DeCustomTab from '@/custom-component/de-tabs/DeCustomTab.vue'
|
||||
import DePreview from '@/components/data-visualization/canvas/DePreview.vue'
|
||||
import { useEmitt } from '@/hooks/web/useEmitt'
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const { tabMoveInActiveId, bashMatrixInfo, editMode } = storeToRefs(dvMainStore)
|
||||
const { tabMoveInActiveId, bashMatrixInfo, editMode, mobileInPc } = storeToRefs(dvMainStore)
|
||||
const tabComponentRef = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
@ -174,7 +174,7 @@ const editableTabsValue = ref(null)
|
||||
const noBorderColor = ref('none')
|
||||
let currentInstance
|
||||
|
||||
const isEditMode = computed(() => editMode.value === 'edit' && isEdit.value)
|
||||
const isEditMode = computed(() => editMode.value === 'edit' && isEdit.value && !mobileInPc.value)
|
||||
|
||||
const calcTabLength = () => {
|
||||
setTimeout(() => {
|
||||
|
@ -53,7 +53,7 @@ const props = defineProps({
|
||||
const { element, view, scale } = toRefs(props)
|
||||
const { t } = useI18n()
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const { curComponent, canvasViewInfo } = storeToRefs(dvMainStore)
|
||||
const { curComponent, canvasViewInfo, mobileInPc } = storeToRefs(dvMainStore)
|
||||
const canEdit = ref(false)
|
||||
const queryConfig = ref()
|
||||
const defaultStyle = {
|
||||
@ -346,7 +346,11 @@ const autoStyle = computed(() => {
|
||||
<div v-if="!listVisible.length" class="no-list-label flex-align-center">
|
||||
<div class="container flex-align-center">
|
||||
将右侧的字段拖拽到这里 或 点击
|
||||
<el-button :disabled="showPosition === 'preview'" @click="addCriteriaConfigOut" text>
|
||||
<el-button
|
||||
:disabled="showPosition === 'preview' || mobileInPc"
|
||||
@click="addCriteriaConfigOut"
|
||||
text
|
||||
>
|
||||
添加查询条件
|
||||
</el-button>
|
||||
</div>
|
||||
|
8
core/core-frontend/src/pages/mobile/App.vue
Normal file
8
core/core-frontend/src/pages/mobile/App.vue
Normal file
@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import configGlobal from '@/components/config-global/src/ConfigGlobal.vue'
|
||||
</script>
|
||||
<template>
|
||||
<config-global>
|
||||
<router-view />
|
||||
</config-global>
|
||||
</template>
|
28
core/core-frontend/src/pages/mobile/main.ts
Normal file
28
core/core-frontend/src/pages/mobile/main.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { createApp } from 'vue'
|
||||
import '../../assets/font/index.css'
|
||||
import '@/style/index.less'
|
||||
import '@/plugins/svg-icon'
|
||||
import 'normalize.css/normalize.css'
|
||||
import App from './App.vue'
|
||||
import { setupI18n } from '@/plugins/vue-i18n'
|
||||
import { setupStore } from '@/store'
|
||||
import { setupRouter } from '@/router/mobile'
|
||||
import { setupElementPlus, setupElementPlusIcons } from '@/plugins/element-plus'
|
||||
// 注册数据大屏组件
|
||||
import { setupCustomComponent } from '@/custom-component'
|
||||
import { installDirective } from '@/directive'
|
||||
import '@/utils/DateUtil'
|
||||
import '@/permissionMobile'
|
||||
const setupAll = async () => {
|
||||
const app = createApp(App)
|
||||
installDirective(app)
|
||||
await setupI18n(app)
|
||||
setupStore(app)
|
||||
setupRouter(app)
|
||||
setupElementPlus(app)
|
||||
setupCustomComponent(app)
|
||||
setupElementPlusIcons(app)
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
setupAll()
|
@ -7,6 +7,7 @@ import { usePermissionStoreWithOut, pathValid, getFirstAuthMenu } from '@/store/
|
||||
import { usePageLoading } from '@/hooks/web/usePageLoading'
|
||||
import { getRoleRouters } from '@/api/common'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { isMobile } from '@/utils/utils'
|
||||
import { interactiveStoreWithOut } from '@/store/modules/interactive'
|
||||
const { wsCache } = useCache()
|
||||
const permissionStore = usePermissionStoreWithOut()
|
||||
@ -23,6 +24,12 @@ const whiteList = ['/login', '/de-link', '/chart-view'] // 不重定向白名单
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
start()
|
||||
loadStart()
|
||||
|
||||
if (isMobile()) {
|
||||
done()
|
||||
loadDone()
|
||||
window.location.href = window.origin + '/mobile.html#/index'
|
||||
}
|
||||
let isDesktop = wsCache.get('app.desktop')
|
||||
if (isDesktop === null) {
|
||||
await appStore.setAppModel()
|
||||
|
49
core/core-frontend/src/permissionMobile.ts
Normal file
49
core/core-frontend/src/permissionMobile.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import router from './router/mobile'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { useNProgress } from '@/hooks/web/useNProgress'
|
||||
import { usePageLoading } from '@/hooks/web/usePageLoading'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { getRoleRouters } from '@/api/common'
|
||||
import { usePermissionStoreWithOut } from '@/store/modules/permission'
|
||||
import { interactiveStoreWithOut } from '@/store/modules/interactive'
|
||||
const permissionStore = usePermissionStoreWithOut()
|
||||
const { wsCache } = useCache()
|
||||
const userStore = useUserStoreWithOut()
|
||||
|
||||
const { start, done } = useNProgress()
|
||||
const interactiveStore = interactiveStoreWithOut()
|
||||
|
||||
const { loadStart, loadDone } = usePageLoading()
|
||||
const whiteList = ['/login'] // 不重定向白名单
|
||||
|
||||
router.beforeEach(async (to, _, next) => {
|
||||
start()
|
||||
loadStart()
|
||||
if (wsCache.get('user.token')) {
|
||||
if (!userStore.getUid) {
|
||||
await userStore.setUser()
|
||||
}
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/index' })
|
||||
} else {
|
||||
const roleRouters = (await getRoleRouters()) || []
|
||||
const routers: any[] = roleRouters as AppCustomRouteRecordRaw[]
|
||||
routers.forEach(item => (item['top'] = true))
|
||||
await permissionStore.generateRoutes(routers as AppCustomRouteRecordRaw[])
|
||||
permissionStore.setIsAddRouters(true)
|
||||
await interactiveStore.initInteractive(true)
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
if (whiteList.includes(to.path)) {
|
||||
next()
|
||||
} else {
|
||||
next('/login') // 否则全部重定向到登录页
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
done()
|
||||
loadDone()
|
||||
})
|
52
core/core-frontend/src/router/mobile.ts
Normal file
52
core/core-frontend/src/router/mobile.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import type { App } from 'vue'
|
||||
|
||||
export const routes: AppRouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: '/',
|
||||
redirect: '/index',
|
||||
hidden: true,
|
||||
meta: {}
|
||||
},
|
||||
{
|
||||
path: '/index',
|
||||
name: 'index',
|
||||
component: () => import('@/views/mobile/index.vue'),
|
||||
hidden: true,
|
||||
meta: {}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
hidden: true,
|
||||
meta: {},
|
||||
component: () => import('@/views/mobile/login/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/panel',
|
||||
name: 'panel',
|
||||
hidden: true,
|
||||
meta: {},
|
||||
component: () => import('@/views/mobile/panel/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/panel/mobile',
|
||||
name: 'mobile',
|
||||
hidden: true,
|
||||
meta: {},
|
||||
component: () => import('@/views/mobile/panel/Mobile.vue')
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: routes as RouteRecordRaw[]
|
||||
})
|
||||
|
||||
export const setupRouter = (app: App<Element>) => {
|
||||
app.use(router)
|
||||
}
|
||||
|
||||
export default router
|
@ -29,6 +29,7 @@ export const dvMainStore = defineStore('dataVisualization', {
|
||||
datasetAreaCollapse: false
|
||||
},
|
||||
editMode: 'edit', // 编辑器模式 edit preview
|
||||
mobileInPc: false,
|
||||
canvasStyleData: { ...deepCopy(DEFAULT_CANVAS_STYLE_DATA_DARK), backgroundColor: null },
|
||||
// 当前展示画布缓存数据
|
||||
componentDataCache: null,
|
||||
@ -196,6 +197,9 @@ export const dvMainStore = defineStore('dataVisualization', {
|
||||
setEditMode(mode) {
|
||||
this.editMode = mode
|
||||
},
|
||||
setMobileInPc(mobileInPc) {
|
||||
this.mobileInPc = mobileInPc
|
||||
},
|
||||
|
||||
setInEditorStatus(status) {
|
||||
this.isInEditor = status
|
||||
|
@ -104,6 +104,7 @@ const hasCurrentRouter = (locations, routers, index) => {
|
||||
kids = router.children
|
||||
return router.path === location || '/' + location === router.path
|
||||
})
|
||||
|
||||
if (isvalid && index < locations.length - 1) {
|
||||
return hasCurrentRouter(locations, kids, index + 1)
|
||||
}
|
||||
|
@ -147,6 +147,41 @@ export function initCanvasData(dvId, busiFlag, callBack) {
|
||||
)
|
||||
}
|
||||
|
||||
export function initCanvasDataMobile(dvId, busiFlag, callBack) {
|
||||
initCanvasDataPrepare(
|
||||
dvId,
|
||||
busiFlag,
|
||||
function ({ canvasDataResult, canvasStyleResult, dvInfo, canvasViewInfoPreview }) {
|
||||
const componentData = canvasDataResult.filter(ele => !!ele.inMobile)
|
||||
canvasDataResult.forEach(ele => {
|
||||
const { mx, my, mSizeX, mSizeY } = ele
|
||||
ele.x = mx
|
||||
ele.y = my
|
||||
ele.sizeX = mSizeX
|
||||
ele.sizeY = mSizeY
|
||||
})
|
||||
dvMainStore.setComponentData(componentData)
|
||||
dvMainStore.setCanvasStyle(canvasStyleResult)
|
||||
dvMainStore.updateCurDvInfo(dvInfo)
|
||||
dvMainStore.setCanvasViewInfo(canvasViewInfoPreview)
|
||||
// 刷新联动信息
|
||||
getPanelAllLinkageInfo(dvInfo.id).then(rsp => {
|
||||
dvMainStore.setNowPanelTrackInfo(rsp.data)
|
||||
})
|
||||
// 刷新跳转信息
|
||||
queryVisualizationJumpInfo(dvInfo.id).then(rsp => {
|
||||
dvMainStore.setNowPanelJumpInfo(rsp.data)
|
||||
})
|
||||
callBack({
|
||||
canvasDataResult: componentData,
|
||||
canvasStyleResult,
|
||||
dvInfo,
|
||||
canvasViewInfoPreview
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function checkIsBatchOptView(viewId) {
|
||||
return curBatchOptComponents.value.includes(viewId)
|
||||
}
|
||||
|
@ -182,8 +182,14 @@ export function getComponentRotatedStyle(style) {
|
||||
}
|
||||
|
||||
export function getCanvasStyle(canvasStyleData) {
|
||||
const { backgroundColorSelect, background, backgroundColor, backgroundImageEnable, fontSize } =
|
||||
canvasStyleData
|
||||
const {
|
||||
backgroundColorSelect,
|
||||
background,
|
||||
backgroundColor,
|
||||
backgroundImageEnable,
|
||||
fontSize,
|
||||
mobileSetting
|
||||
} = canvasStyleData
|
||||
const style = { fontSize: fontSize + 'px', color: canvasStyleData.color }
|
||||
// 仪表板默认色#f5f6f7 大屏默认配色 #1a1a1a
|
||||
let colorRGBA = dvMainStore.dvInfo.type === 'dashboard' ? '#f5f6f7' : '#1a1a1a'
|
||||
@ -195,6 +201,17 @@ export function getCanvasStyle(canvasStyleData) {
|
||||
} else {
|
||||
style['background-color'] = colorRGBA
|
||||
}
|
||||
|
||||
if (dvMainStore.mobileInPc && mobileSetting?.customSetting) {
|
||||
const { backgroundType, color, alpha, imageUrl } = mobileSetting
|
||||
if (backgroundType === 'image' && typeof imageUrl === 'string') {
|
||||
style['background'] = `url(${imgUrlTrans(imageUrl)}) no-repeat`
|
||||
} else if (backgroundType === 'color') {
|
||||
const colorRGBA = hexColorToRGBA(color, alpha === undefined ? 100 : alpha)
|
||||
style['background'] = colorRGBA
|
||||
}
|
||||
}
|
||||
console.log('style', style)
|
||||
return style
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,11 @@ export const isLarkPlatform = () => {
|
||||
return !!getQueryString('state') && !!getQueryString('code')
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
return navigator.userAgent.match(
|
||||
/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
|
||||
)
|
||||
}
|
||||
export function cutTargetTree(tree: BusiTreeNode[], targetId: string | number) {
|
||||
tree.forEach((node, index) => {
|
||||
if (node.id === targetId) {
|
||||
|
@ -0,0 +1,208 @@
|
||||
<script lang="ts" setup>
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import { ref, reactive, unref, onMounted, watch } from 'vue'
|
||||
import { COLOR_PANEL } from '@/views/chart/components/editor/util/chart'
|
||||
import { imgUrlTrans } from '@/utils/imgUtils'
|
||||
import ImgViewDialog from '@/custom-component/ImgViewDialog.vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useEmitt } from '@/hooks/web/useEmitt'
|
||||
import { beforeUploadCheck, uploadFileResult } from '@/api/staticResource'
|
||||
import { snapshotStoreWithOut } from '@/store/modules/data-visualization/snapshot'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
const MOBILE_SETTING = {
|
||||
customSetting: false,
|
||||
color: '#ffffff',
|
||||
alpha: 100,
|
||||
imageUrl: null,
|
||||
backgroundType: 'image'
|
||||
}
|
||||
const fileList = ref([])
|
||||
|
||||
const mobileSetting = reactive({ ...MOBILE_SETTING })
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const snapshotStore = snapshotStoreWithOut()
|
||||
const { canvasStyleData } = storeToRefs(dvMainStore)
|
||||
|
||||
const init = () => {
|
||||
const { imageUrl } = canvasStyleData.value.mobileSetting || {}
|
||||
fileList.value = imageUrl ? [{ url: imgUrlTrans(imageUrl) }] : []
|
||||
}
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
|
||||
const commitStyle = () => {
|
||||
const canvasStyleDataCopy = cloneDeep(canvasStyleData.value)
|
||||
canvasStyleDataCopy.mobileSetting = unref(mobileSetting)
|
||||
dvMainStore.setCanvasStyle(canvasStyleDataCopy)
|
||||
useEmitt().emitter.emit('onMobileStatusChange', {
|
||||
type: 'setCanvasStyle',
|
||||
value: JSON.parse(JSON.stringify(unref(canvasStyleDataCopy)))
|
||||
})
|
||||
snapshotStore.recordSnapshotCache()
|
||||
}
|
||||
|
||||
const upload = file => {
|
||||
return uploadFileResult(file.file, fileUrl => {
|
||||
mobileSetting.imageUrl = fileUrl
|
||||
commitStyle()
|
||||
})
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogImageUrl = ref('')
|
||||
const handlePictureCardPreview = file => {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleRemove = () => {
|
||||
mobileSetting.imageUrl = null
|
||||
dialogImageUrl.value = ''
|
||||
fileList.value = []
|
||||
commitStyle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-bg-setting">
|
||||
<div class="is-custom">
|
||||
<el-checkbox v-model="mobileSetting.customSetting" label="自定义移动端背景" />
|
||||
</div>
|
||||
<div class="bg-type">
|
||||
<el-radio-group
|
||||
v-model="mobileSetting.backgroundType"
|
||||
:disabled="!mobileSetting.customSetting"
|
||||
@change="commitStyle"
|
||||
>
|
||||
<el-radio label="color">{{ $t('chart.color') }}</el-radio>
|
||||
<el-radio label="image">图片</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-show="mobileSetting.backgroundType === 'color'" class="bg-color-config">
|
||||
<el-color-picker
|
||||
v-model="mobileSetting.color"
|
||||
:predefine="COLOR_PANEL"
|
||||
:disabled="!mobileSetting.customSetting"
|
||||
@change="commitStyle"
|
||||
/>
|
||||
<span> 不透明度 </span>
|
||||
<el-slider
|
||||
v-model="mobileSetting.alpha"
|
||||
show-input
|
||||
:show-input-controls="false"
|
||||
@change="commitStyle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
disabled: !mobileSetting.customSetting
|
||||
}"
|
||||
v-show="mobileSetting.backgroundType === 'image'"
|
||||
class="upload-img"
|
||||
>
|
||||
<el-upload
|
||||
action=""
|
||||
accept=".jpeg,.jpg,.png,.gif,.svg"
|
||||
class="avatar-uploader"
|
||||
list-type="picture-card"
|
||||
:http-request="upload"
|
||||
:on-preview="handlePictureCardPreview"
|
||||
:on-remove="handleRemove"
|
||||
:before-upload="beforeUploadCheck"
|
||||
:file-list="fileList"
|
||||
:disabled="!mobileSetting.customSetting"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<img-view-dialog v-model="dialogVisible" :image-url="dialogImageUrl" />
|
||||
</div>
|
||||
<div v-show="mobileSetting.backgroundType === 'image'" class="image-hint">
|
||||
当前支持jpeg,jpg,png,gif,svg文件,大小15M内
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mobile-bg-setting {
|
||||
width: 50%;
|
||||
.bg-color-config {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > :nth-child(2) {
|
||||
margin: 0 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
width: 90px;
|
||||
height: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatar-uploader {
|
||||
width: 90px;
|
||||
:deep(.ed-upload) {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
line-height: 90px;
|
||||
}
|
||||
|
||||
:deep(.ed-upload-list li) {
|
||||
width: 80px !important;
|
||||
height: 80px !important;
|
||||
}
|
||||
|
||||
:deep(.ed-upload--picture-card) {
|
||||
background: #eff0f1;
|
||||
border: 1px dashed #dee0e3;
|
||||
border-radius: 4px;
|
||||
|
||||
.ed-icon {
|
||||
color: #1f2329;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ed-icon {
|
||||
color: #3370ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-img {
|
||||
margin-top: 8px;
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: #8f959e;
|
||||
|
||||
:deep(.avatar-uploader) {
|
||||
width: 90px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.ed-upload--picture-card) {
|
||||
cursor: not-allowed;
|
||||
.ed-icon {
|
||||
color: #bbbfc4;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ed-icon {
|
||||
color: #8f959e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-hint {
|
||||
color: #8f959e;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
250
core/core-frontend/src/views/dashboard/MobileConfigPanel.vue
Normal file
250
core/core-frontend/src/views/dashboard/MobileConfigPanel.vue
Normal file
@ -0,0 +1,250 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, unref, onBeforeUnmount, computed } from 'vue'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import MobileBackgroundSelector from './MobileBackgroundSelector.vue'
|
||||
import { useEmitt } from '@/hooks/web/useEmitt'
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import { getStyle } from '@/utils/style'
|
||||
import findComponent from '@/utils/components'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const { componentData, canvasStyleData, canvasViewInfo, dvInfo } = storeToRefs(dvMainStore)
|
||||
const mobileLoading = ref(true)
|
||||
const activeName = ref('hide')
|
||||
|
||||
const emits = defineEmits(['pcMode'])
|
||||
|
||||
const getComponentStyleDefault = style => {
|
||||
return getStyle(style, ['top', 'left', 'width', 'height', 'rotate'])
|
||||
}
|
||||
const mobileStatusChange = (type, value) => {
|
||||
const iframe = document.querySelector('iframe')
|
||||
if (iframe) {
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type,
|
||||
value
|
||||
},
|
||||
'*'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoad = () => {
|
||||
mobileStatusChange(
|
||||
'panelInit',
|
||||
JSON.parse(
|
||||
JSON.stringify({
|
||||
componentData: JSON.parse(
|
||||
JSON.stringify(unref(componentData.value.filter(ele => !!ele.inMobile)))
|
||||
),
|
||||
canvasStyleData: JSON.parse(JSON.stringify(unref(canvasStyleData))),
|
||||
canvasViewInfo: JSON.parse(JSON.stringify(unref(canvasViewInfo))),
|
||||
dvInfo: JSON.parse(JSON.stringify(unref(dvInfo)))
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const componentDataNotInMobile = computed(() => {
|
||||
return componentData.value.filter(ele => !ele.inMobile)
|
||||
})
|
||||
|
||||
const hanedleMessage = event => {
|
||||
if (event.data.type === 'panelInit') {
|
||||
mobileLoading.value = false
|
||||
handleLoad()
|
||||
}
|
||||
|
||||
if (event.data.type === 'delFromMobile') {
|
||||
componentData.value.some(ele => {
|
||||
if (ele.id === event.data.value) {
|
||||
ele.inMobile = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
if (event.data.type === 'mobileSaveFromMobile') {
|
||||
componentData.value.forEach(ele => {
|
||||
const com = event.data.value[ele.id]
|
||||
if (!!com) {
|
||||
const { x, y, sizeX, sizeY } = com
|
||||
ele.mx = x
|
||||
ele.my = y
|
||||
ele.mSizeX = sizeX
|
||||
ele.mSizeY = sizeY
|
||||
}
|
||||
})
|
||||
|
||||
eventBus.emit('save')
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', hanedleMessage)
|
||||
dvMainStore.setMobileInPc(true)
|
||||
useEmitt({
|
||||
name: 'onMobileStatusChange',
|
||||
callback: ({ type, value }) => {
|
||||
mobileStatusChange(type, value)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
dvMainStore.setMobileInPc(false)
|
||||
window.removeEventListener('message', hanedleMessage)
|
||||
})
|
||||
|
||||
const addToMobile = com => {
|
||||
com.inMobile = true
|
||||
mobileStatusChange('addToMobile', JSON.parse(JSON.stringify(unref(com))))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-config-panel">
|
||||
<div class="mobile-to-pc">
|
||||
<el-icon class="custom-el-icon back-icon" @click="emits('pcMode')">
|
||||
<Icon class="toolbar-icon" name="icon_left_outlined" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="mobile-save">
|
||||
<el-button type="primary" @click="mobileStatusChange('mobileSave', undefined)"
|
||||
>保存</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="mobile-canvas">
|
||||
<div class="config-panel-title">{{ dvInfo.name }}</div>
|
||||
<div class="config-panel-content" v-loading="mobileLoading">
|
||||
<iframe src="./mobile.html#/panel" frameborder="0" width="360" height="570" />
|
||||
</div>
|
||||
<div class="config-panel-foot"></div>
|
||||
</div>
|
||||
<div class="mobile-com-list">
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane label="隐藏的组件" name="hide" />
|
||||
<el-tab-pane label="样式设置" name="style" />
|
||||
</el-tabs>
|
||||
<template v-if="activeName === 'hide'">
|
||||
<div
|
||||
:style="{ height: '200px', width: '31%' }"
|
||||
class="mobile-wrapper-inner-adaptor"
|
||||
v-for="config in componentDataNotInMobile"
|
||||
:key="config.id"
|
||||
>
|
||||
<component
|
||||
:is="findComponent(config['component'])"
|
||||
ref="component"
|
||||
class="component"
|
||||
:view="canvasViewInfo[config.id]"
|
||||
:canvas-style-data="canvasStyleData"
|
||||
:dv-info="dvInfo"
|
||||
:dv-type="dvInfo.type"
|
||||
:canvas-view-info="canvasViewInfo"
|
||||
:prop-value="config?.propValue"
|
||||
:element="config"
|
||||
:request="config?.request"
|
||||
:style="getComponentStyleDefault(config?.style)"
|
||||
:linkage="config?.linkage"
|
||||
show-position="preview"
|
||||
:disabled="true"
|
||||
:is-edit="false"
|
||||
/>
|
||||
<div class="mobile-com-mask"></div>
|
||||
|
||||
<div class="pc-select-to-mobile" v-if="!mobileLoading" @click="addToMobile(config)"></div>
|
||||
</div>
|
||||
</template>
|
||||
<MobileBackgroundSelector v-else></MobileBackgroundSelector>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mobile-config-panel {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: relative;
|
||||
|
||||
.mobile-to-pc {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-save {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
}
|
||||
.mobile-canvas {
|
||||
border-radius: 30px;
|
||||
width: 370px;
|
||||
min-height: 600px;
|
||||
max-height: 700px;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
background-size: 100% 100% !important;
|
||||
padding: 5px;
|
||||
height: 777px;
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 100px;
|
||||
|
||||
.config-panel-title {
|
||||
margin-top: 30px;
|
||||
background: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-com-list {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
width: calc(100% - 600px);
|
||||
max-height: 700px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: #f5f6f7;
|
||||
padding: 16px;
|
||||
padding-top: 0;
|
||||
|
||||
:deep(.ed-tabs) {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.mobile-wrapper-inner-adaptor {
|
||||
position: relative;
|
||||
margin-right: 24px;
|
||||
margin-bottom: 24px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.mobile-com-mask {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.pc-select-to-mobile {
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 4px;
|
||||
z-index: 11;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -5,6 +5,8 @@ import { storeToRefs } from 'pinia'
|
||||
import findComponent from '../../utils/components'
|
||||
import DvSidebar from '../../components/visualization/DvSidebar.vue'
|
||||
import router from '@/router'
|
||||
import MobileConfigPanel from './MobileConfigPanel.vue'
|
||||
import { useEmitt } from '@/hooks/web/useEmitt'
|
||||
import DbToolbar from '@/components/dashboard/DbToolbar.vue'
|
||||
import ViewEditor from '@/views/chart/components/editor/index.vue'
|
||||
import { getDatasetTree } from '@/api/dataset'
|
||||
@ -33,7 +35,6 @@ const eventCheck = e => {
|
||||
}
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const snapshotStore = snapshotStoreWithOut()
|
||||
|
||||
const {
|
||||
componentData,
|
||||
curComponent,
|
||||
@ -72,8 +73,20 @@ const checkPer = async resourceId => {
|
||||
await interactiveStore.setInteractive(request)
|
||||
return check(wsCache.get('panel-weight'), resourceId, 4)
|
||||
}
|
||||
|
||||
const mobileConfig = ref(false)
|
||||
|
||||
const onMobileConfig = () => {
|
||||
mobileConfig.value = true
|
||||
}
|
||||
// 全局监听按键事件
|
||||
onMounted(async () => {
|
||||
useEmitt({
|
||||
name: 'mobileConfig',
|
||||
callback: () => {
|
||||
onMobileConfig()
|
||||
}
|
||||
})
|
||||
window.addEventListener('storage', eventCheck)
|
||||
const resourceId = embeddedStore.resourceId || router.currentRoute.value.query.resourceId
|
||||
const pid = embeddedStore.pid || router.currentRoute.value.query.pid
|
||||
@ -139,7 +152,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dv-common-layout dv-teleport-query">
|
||||
<div class="dv-common-layout dv-teleport-query" v-if="!mobileConfig">
|
||||
<DbToolbar />
|
||||
<el-container
|
||||
class="dv-layout-container"
|
||||
@ -205,6 +218,7 @@ onUnmounted(() => {
|
||||
</dv-sidebar>
|
||||
</el-container>
|
||||
</div>
|
||||
<MobileConfigPanel @pcMode="mobileConfig = false" v-else></MobileConfigPanel>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
|
130
core/core-frontend/src/views/mobile/directory/index.vue
Normal file
130
core/core-frontend/src/views/mobile/directory/index.vue
Normal file
@ -0,0 +1,130 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import { BusiTreeRequest } from '@/models/tree/TreeNode'
|
||||
import { interactiveStoreWithOut } from '@/store/modules/interactive'
|
||||
import { useRouter } from 'vue-router'
|
||||
import VanCell from 'vant/es/cell'
|
||||
import VanSticky from 'vant/es/sticky'
|
||||
import VanSearch from 'vant/es/search'
|
||||
import VanCellGroup from 'vant/es/cell-group'
|
||||
import VanNavBar from 'vant/es/nav-bar'
|
||||
import 'vant/es/cell/style'
|
||||
import 'vant/es/cell-group/style'
|
||||
import 'vant/es/nav-bar/style'
|
||||
import 'vant/es/sticky/style'
|
||||
import 'vant/es/search/style'
|
||||
const keywords = ref()
|
||||
const anyManage = ref(false)
|
||||
const rootManage = ref(false)
|
||||
const tableData = ref([])
|
||||
const directName = ref([])
|
||||
const directId = ref([])
|
||||
const activeDirectName = ref('')
|
||||
const interactiveStore = interactiveStoreWithOut()
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const { dvInfo } = storeToRefs(dvMainStore)
|
||||
|
||||
const dfsTree = (ids, arr) => {
|
||||
const id = ids.shift()
|
||||
return arr.reduce((pre, ele) => {
|
||||
if (id && ele.id === id) {
|
||||
if (!ids.length) {
|
||||
return ele.children || []
|
||||
}
|
||||
const children = dfsTree([...ids], ele.children || [])
|
||||
pre = children || []
|
||||
}
|
||||
return pre
|
||||
}, [])
|
||||
}
|
||||
|
||||
const activeTableData = computed(() => {
|
||||
return directId.value.length ? dfsTree([...directId.value], tableData.value) : tableData.value
|
||||
})
|
||||
|
||||
const emits = defineEmits(['hiddenTabbar', 'setLoading'])
|
||||
const onClickLeft = () => {
|
||||
directName.value.pop()
|
||||
activeDirectName.value = directName.value[directName.value.length - 1]
|
||||
directId.value.pop()
|
||||
if (!!directName.value.length) {
|
||||
emits('hiddenTabbar', false)
|
||||
}
|
||||
}
|
||||
const router = useRouter()
|
||||
|
||||
const handleCellClick = ele => {
|
||||
router.push({
|
||||
path: '/panel/mobile',
|
||||
query: {
|
||||
dvId: ele.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const dataClick = val => {
|
||||
directName.value.push(val.name)
|
||||
activeDirectName.value = val.name
|
||||
directId.value.push(val.id)
|
||||
if (val.leaf) {
|
||||
emits('hiddenTabbar', true)
|
||||
handleCellClick(val)
|
||||
}
|
||||
}
|
||||
|
||||
const getTree = async () => {
|
||||
const request = { busiFlag: 'dashboard' } as BusiTreeRequest
|
||||
await interactiveStore.setInteractive(request)
|
||||
const interactiveData = interactiveStore.getPanel
|
||||
const nodeData = interactiveData.treeNodes
|
||||
rootManage.value = interactiveData.rootManage
|
||||
anyManage.value = interactiveData.anyManage
|
||||
if (dvInfo.value && dvInfo.value.id && !JSON.stringify(nodeData).includes(dvInfo.value.id)) {
|
||||
dvMainStore.resetDvInfo()
|
||||
}
|
||||
if (nodeData.length && nodeData[0]['id'] === '0' && nodeData[0]['name'] === 'root') {
|
||||
tableData.value = nodeData[0]['children'] || []
|
||||
return
|
||||
}
|
||||
tableData.value = nodeData
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getTree()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<van-sticky>
|
||||
<van-search
|
||||
v-if="!directName.length"
|
||||
v-model="keywords"
|
||||
shape="round"
|
||||
placeholder="请输入仪表板名称"
|
||||
/>
|
||||
<van-nav-bar
|
||||
v-else
|
||||
:title="activeDirectName"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
</van-sticky>
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
v-for="ele in activeTableData"
|
||||
:key="ele.id"
|
||||
size="large"
|
||||
@click="dataClick(ele)"
|
||||
:title="ele.name"
|
||||
:is-link="!ele.leaf"
|
||||
:icon="ele.leaf ? 'bar-chart-o' : 'list-switch'"
|
||||
/>
|
||||
</van-cell-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
127
core/core-frontend/src/views/mobile/home/index.vue
Normal file
127
core/core-frontend/src/views/mobile/home/index.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, reactive } from 'vue'
|
||||
import { interactiveStoreWithOut } from '@/store/modules/interactive'
|
||||
import { shortcutOption } from '@/views/workbranch/ShortcutOption'
|
||||
import { XpackComponent } from '@/components/plugin'
|
||||
import { useRouter } from 'vue-router'
|
||||
import VanTabs from 'vant/es/tabs'
|
||||
import VanTab from 'vant/es/tab'
|
||||
import VanCell from 'vant/es/cell'
|
||||
import VanSticky from 'vant/es/sticky'
|
||||
import VanCellGroup from 'vant/es/cell-group'
|
||||
import 'vant/es/sticky/style'
|
||||
import 'vant/es/tab/style'
|
||||
import 'vant/es/tabs/style'
|
||||
import 'vant/es/cell/style'
|
||||
import 'vant/es/cell-group/style'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref('recent')
|
||||
const state = reactive({
|
||||
tableData: [],
|
||||
curTypeList: []
|
||||
})
|
||||
const interactiveStore = interactiveStoreWithOut()
|
||||
|
||||
const emits = defineEmits(['setLoading'])
|
||||
const loadTableData = () => {
|
||||
emits('setLoading', true)
|
||||
shortcutOption
|
||||
.loadData({ type: 'panel', keyword: '', asc: false })
|
||||
.then(res => {
|
||||
state.tableData = res.data
|
||||
})
|
||||
.finally(() => {
|
||||
emits('setLoading', false)
|
||||
})
|
||||
}
|
||||
|
||||
const tablePaneList = ref([
|
||||
{ title: '最近使用', name: 'recent', disabled: false },
|
||||
{ title: '我的收藏', name: 'store', disabled: false }
|
||||
])
|
||||
|
||||
const panelLoad = paneInfo => {
|
||||
tablePaneList.value.push({
|
||||
title: paneInfo.title,
|
||||
name: paneInfo.name,
|
||||
disabled: tablePaneList.value[1].disabled
|
||||
})
|
||||
}
|
||||
const busiDataMap = computed(() => interactiveStore.getData)
|
||||
|
||||
const getBusiListWithPermission = () => {
|
||||
const baseFlagList = ['panel', 'screen', 'dataset', 'datasource']
|
||||
const busiFlagList: string[] = []
|
||||
for (const key in busiDataMap.value) {
|
||||
if (busiDataMap.value[key].menuAuth) {
|
||||
busiFlagList.push(baseFlagList[parseInt(key)])
|
||||
}
|
||||
}
|
||||
tablePaneList.value[0].disabled = !busiFlagList?.length
|
||||
tablePaneList.value[1].disabled =
|
||||
!busiFlagList.includes('panel') && !busiFlagList.includes('screen')
|
||||
return busiFlagList
|
||||
}
|
||||
|
||||
const busiAuthList = getBusiListWithPermission()
|
||||
|
||||
const handleClick = ({ name, disabled }) => {
|
||||
if (disabled) return
|
||||
if (name === 'recent' || name === 'store') {
|
||||
emits('setLoading', true)
|
||||
shortcutOption.setBusiFlag(name)
|
||||
loadTableData()
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
!!busiAuthList.length &&
|
||||
handleClick({
|
||||
name: 'recent',
|
||||
disabled: false
|
||||
})
|
||||
})
|
||||
|
||||
const handleCellClick = ele => {
|
||||
router.push({
|
||||
path: '/panel/mobile',
|
||||
query: {
|
||||
dvId: ele.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const formatterTime = val => {
|
||||
return new Date(val).toLocaleString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-panel-list">
|
||||
<van-sticky>
|
||||
<van-tabs @click-tab="handleClick" v-model:active="activeTab">
|
||||
<van-tab
|
||||
v-for="item in tablePaneList"
|
||||
:key="item.name"
|
||||
:disabled="item.disabled"
|
||||
:name="item.name"
|
||||
:title="item.title"
|
||||
></van-tab>
|
||||
</van-tabs>
|
||||
</van-sticky>
|
||||
<van-cell-group>
|
||||
<van-cell
|
||||
@click="handleCellClick(ele)"
|
||||
v-for="ele in state.tableData"
|
||||
:key="ele.id"
|
||||
size="large"
|
||||
:title="ele.name"
|
||||
:value="formatterTime(ele.lastEditTime)"
|
||||
icon="bar-chart-o"
|
||||
/>
|
||||
</van-cell-group>
|
||||
<div style="width: 100%; height: 50px"></div>
|
||||
<XpackComponent jsname="c2hhcmUtcGFuZWw=" @loaded="panelLoad" />
|
||||
</div>
|
||||
</template>
|
56
core/core-frontend/src/views/mobile/index.vue
Normal file
56
core/core-frontend/src/views/mobile/index.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import Home from './home/index.vue'
|
||||
import Directory from './directory/index.vue'
|
||||
import Personal from './personal/index.vue'
|
||||
import VanTabbar from 'vant/es/tabbar'
|
||||
import VanTabbarItem from 'vant/es/tabbar-item'
|
||||
import VanOverlay from 'vant/es/overlay'
|
||||
import VanLoading from 'vant/es/loading'
|
||||
import 'vant/es/tabbar-item/style'
|
||||
import 'vant/es/tabbar/style'
|
||||
import 'vant/es/overlay/style'
|
||||
import 'vant/es/loading/style'
|
||||
|
||||
const activeTabbar = ref('home')
|
||||
const showLoading = ref(false)
|
||||
const hiddenTabbar = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mobile-index">
|
||||
<Home v-if="activeTabbar === 'home'" @setLoading="val => (showLoading = val)"></Home>
|
||||
<Directory
|
||||
v-else-if="activeTabbar === 'direct'"
|
||||
@setLoading="val => (showLoading = val)"
|
||||
@hiddenTabbar="val => (hiddenTabbar = val)"
|
||||
></Directory>
|
||||
<Personal v-else-if="activeTabbar === 'user'"> </Personal>
|
||||
<van-tabbar v-if="!hiddenTabbar" v-model="activeTabbar">
|
||||
<van-tabbar-item name="home" icon="wap-home-o">首页</van-tabbar-item>
|
||||
<van-tabbar-item name="direct" icon="bars">目录</van-tabbar-item>
|
||||
<van-tabbar-item name="user" icon="user-o">我的</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
<van-overlay v-model:show="showLoading">
|
||||
<div style="display: flex; align-items: center; justify-content: center; height: 100%">
|
||||
<van-loading type="spinner" />
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
body {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
html {
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
.mobile-index {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
132
core/core-frontend/src/views/mobile/login/index.vue
Normal file
132
core/core-frontend/src/views/mobile/login/index.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import VanCellGroup from 'vant/es/cell-group'
|
||||
import { showToast } from 'vant'
|
||||
import { loginApi, queryDekey } from '@/api/login'
|
||||
import { useAppStoreWithOut } from '@/store/modules/app'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import { useCache } from '@/hooks/web/useCache'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { rsaEncryp } from '@/utils/encryption'
|
||||
import VanForm from 'vant/es/form'
|
||||
import VanField from 'vant/es/field'
|
||||
import VanButton from 'vant/es/button'
|
||||
import 'vant/es/button/style'
|
||||
import 'vant/es/field/style'
|
||||
import 'vant/es/form/style'
|
||||
import 'vant/es/cell-group/style'
|
||||
|
||||
const { wsCache } = useCache()
|
||||
const appStore = useAppStoreWithOut()
|
||||
const userStore = useUserStoreWithOut()
|
||||
const router = useRouter()
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const duringLogin = ref(false)
|
||||
|
||||
const checkUsername = value => {
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
const pattern = /^[a-zA-Z0-9][a-zA-Z0-9\@._-]*$/
|
||||
const reg = new RegExp(pattern)
|
||||
return reg.test(value)
|
||||
}
|
||||
|
||||
const validatePwd = value => {
|
||||
if (!value) {
|
||||
return true
|
||||
}
|
||||
const pattern =
|
||||
/^.*(?=.{6,20})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[~!@#$%^&*()_+\-\={}|":<>?`[\];',.\/])[a-zA-Z0-9~!@#$%^&*()_+\-\={}|":<>?`[\];',.\/]*$/
|
||||
const regep = new RegExp(pattern)
|
||||
return regep.test(value)
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!checkUsername(username.value) || !validatePwd(password.value)) {
|
||||
showToast('用户名或密码错误')
|
||||
return
|
||||
}
|
||||
const name = username.value.trim()
|
||||
const pwd = password.value
|
||||
if (!wsCache.get(appStore.getDekey)) {
|
||||
const res = await queryDekey()
|
||||
wsCache.set(appStore.getDekey, res.data)
|
||||
}
|
||||
const param = { name: rsaEncryp(name), pwd: rsaEncryp(pwd) }
|
||||
duringLogin.value = true
|
||||
loginApi(param)
|
||||
.then(res => {
|
||||
const { token, exp } = res.data
|
||||
userStore.setToken(token)
|
||||
userStore.setExp(exp)
|
||||
router.push({ path: '/index' })
|
||||
})
|
||||
.catch(() => {
|
||||
duringLogin.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="de-mobile-login" v-loading="duringLogin">
|
||||
<div class="mobile-login-content">
|
||||
<div class="mobile-login-welcome">用户登录</div>
|
||||
<van-form @submit="onSubmit">
|
||||
<van-cell-group inset>
|
||||
<van-field
|
||||
v-model="username"
|
||||
name="用户名"
|
||||
label="用户名"
|
||||
placeholder="用户名"
|
||||
:rules="[{ required: true, message: '请填写用户名' }]"
|
||||
/>
|
||||
<van-field
|
||||
v-model="password"
|
||||
type="password"
|
||||
name="密码"
|
||||
label="密码"
|
||||
placeholder="密码"
|
||||
:rules="[{ required: true, message: '请填写密码' }]"
|
||||
/>
|
||||
</van-cell-group>
|
||||
<div style="margin: 16px">
|
||||
<van-button round block type="primary" native-type="submit"> 提交 </van-button>
|
||||
</div>
|
||||
</van-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.de-mobile-login {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-image: url(../../../assets/logo-bg.jpg);
|
||||
|
||||
.mobile-login-content {
|
||||
background-color: #fff;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
opacity: 0.95;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.van-field) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-login-welcome {
|
||||
padding-left: 15px;
|
||||
font-size: x-large;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
96
core/core-frontend/src/views/mobile/panel/Mobile.vue
Normal file
96
core/core-frontend/src/views/mobile/panel/Mobile.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { initCanvasDataMobile } from '@/utils/canvasUtils'
|
||||
import { ref, nextTick, onBeforeMount } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import DePreview from '@/components/data-visualization/canvas/DePreview.vue'
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import VanSticky from 'vant/es/sticky'
|
||||
import VanNavBar from 'vant/es/nav-bar'
|
||||
import 'vant/es/nav-bar/style'
|
||||
import 'vant/es/sticky/style'
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
const state = reactive({
|
||||
canvasDataPreview: null,
|
||||
canvasStylePreview: null,
|
||||
canvasViewInfoPreview: null,
|
||||
dvInfo: {
|
||||
name: ''
|
||||
},
|
||||
curPreviewGap: 0
|
||||
})
|
||||
const dataInitState = ref(true)
|
||||
const dashboardPreview = ref(null)
|
||||
|
||||
const loadCanvasData = (dvId, weight?) => {
|
||||
dataInitState.value = false
|
||||
initCanvasDataMobile(
|
||||
dvId,
|
||||
'dashboard',
|
||||
function ({
|
||||
canvasDataResult,
|
||||
canvasStyleResult,
|
||||
dvInfo,
|
||||
canvasViewInfoPreview,
|
||||
curPreviewGap
|
||||
}) {
|
||||
dvInfo['weight'] = weight
|
||||
state.canvasDataPreview = canvasDataResult
|
||||
state.canvasStylePreview = canvasStyleResult
|
||||
state.canvasViewInfoPreview = canvasViewInfoPreview
|
||||
state.dvInfo = dvInfo
|
||||
state.curPreviewGap = curPreviewGap
|
||||
dataInitState.value = true
|
||||
nextTick(() => {
|
||||
dashboardPreview.value.restore()
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onBeforeMount(() => {
|
||||
dvMainStore.setMobileInPc(true)
|
||||
const dvId = route.query.dvId as unknown as string
|
||||
loadCanvasData(dvId)
|
||||
})
|
||||
|
||||
const onClickLeft = () => {
|
||||
router.replace({
|
||||
path: '/index'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dv-common-layout-mobile">
|
||||
<van-sticky>
|
||||
<van-nav-bar
|
||||
:title="state.dvInfo.name"
|
||||
left-text="返回"
|
||||
left-arrow
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
</van-sticky>
|
||||
<de-preview
|
||||
ref="dashboardPreview"
|
||||
v-if="state.canvasStylePreview && dataInitState"
|
||||
:dv-info="state.dvInfo"
|
||||
:cur-gap="state.curPreviewGap"
|
||||
:component-data="state.canvasDataPreview"
|
||||
:canvas-style-data="state.canvasStylePreview"
|
||||
:canvas-view-info="state.canvasViewInfoPreview"
|
||||
:download-status="false"
|
||||
></de-preview>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.dv-common-layout-mobile {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
33
core/core-frontend/src/views/mobile/panel/MobileInPc.vue
Normal file
33
core/core-frontend/src/views/mobile/panel/MobileInPc.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue'
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import DeCanvas from '@/views/canvas/DeCanvas.vue'
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
|
||||
const { componentData, canvasStyleData, canvasViewInfo } = storeToRefs(dvMainStore)
|
||||
|
||||
const state = reactive({
|
||||
canvasId: 'canvas-main'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dv-common-layout-mobile">
|
||||
<de-canvas
|
||||
style="overflow-x: hidden"
|
||||
:canvas-id="state.canvasId"
|
||||
:component-data="componentData"
|
||||
:canvas-style-data="canvasStyleData"
|
||||
:canvas-view-info="canvasViewInfo"
|
||||
></de-canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
.dv-common-layout-mobile {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
95
core/core-frontend/src/views/mobile/panel/index.vue
Normal file
95
core/core-frontend/src/views/mobile/panel/index.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, ref, onBeforeUnmount } from 'vue'
|
||||
import { useEmitt } from '@/hooks/web/useEmitt'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
import { dvMainStoreWithOut } from '@/store/modules/data-visualization/dvMain'
|
||||
import DePreviewMobile from './MobileInPc.vue'
|
||||
const panelInit = ref(false)
|
||||
const dvMainStore = dvMainStoreWithOut()
|
||||
|
||||
const checkItemPosition = component => {
|
||||
component.x = 1
|
||||
component.sizeX = 36
|
||||
component.y = dvMainStore.componentData.reduce((pre, next) => {
|
||||
return Math.max(pre, next.y + next.sizeY)
|
||||
}, 1)
|
||||
}
|
||||
|
||||
const hanedleMessage = event => {
|
||||
if (event.data.type === 'panelInit') {
|
||||
const { componentData, canvasStyleData, dvInfo, canvasViewInfo } = event.data.value
|
||||
componentData.forEach(ele => {
|
||||
const { mx, my, mSizeX, mSizeY } = ele
|
||||
ele.x = mx
|
||||
ele.y = my
|
||||
ele.sizeX = mSizeX
|
||||
ele.sizeY = mSizeY
|
||||
})
|
||||
dvMainStore.setComponentData(componentData)
|
||||
dvMainStore.setMobileInPc(true)
|
||||
dvMainStore.setCanvasStyle(canvasStyleData)
|
||||
dvMainStore.updateCurDvInfo(dvInfo)
|
||||
dvMainStore.setCanvasViewInfo(canvasViewInfo)
|
||||
panelInit.value = true
|
||||
}
|
||||
|
||||
if (event.data.type === 'addToMobile') {
|
||||
const component = event.data.value
|
||||
checkItemPosition(component)
|
||||
dvMainStore.componentData.push(component)
|
||||
}
|
||||
|
||||
if (event.data.type === 'setCanvasStyle') {
|
||||
dvMainStore.setCanvasStyle(event.data.value)
|
||||
}
|
||||
|
||||
if (event.data.type === 'mobileSave') {
|
||||
window.top.postMessage(
|
||||
{
|
||||
type: 'mobileSaveFromMobile',
|
||||
value: dvMainStore.componentData.reduce((pre, next) => {
|
||||
const { x, y, sizeX, sizeY, id } = next
|
||||
pre[id] = { x, y, sizeX, sizeY }
|
||||
return pre
|
||||
}, {})
|
||||
},
|
||||
'*'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
window.top.postMessage({ type: 'panelInit', value: true }, '*')
|
||||
window.addEventListener('message', hanedleMessage)
|
||||
useEmitt({
|
||||
name: 'onMobileStatusChange',
|
||||
callback: ({ type, value }) => {
|
||||
mobileStatusChange(type, value)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const mobileStatusChange = (type, value) => {
|
||||
window.top.postMessage({ type, value }, '*')
|
||||
if (type === 'delFromMobile') {
|
||||
eventBus.emit('removeMatrixItemById-canvas-main', value)
|
||||
}
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', hanedleMessage)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel-mobile">
|
||||
<de-preview-mobile v-if="panelInit"></de-preview-mobile>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.panel-mobile {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
78
core/core-frontend/src/views/mobile/personal/index.vue
Normal file
78
core/core-frontend/src/views/mobile/personal/index.vue
Normal file
@ -0,0 +1,78 @@
|
||||
<script lang="ts" setup>
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
import userImg from '@/assets/img/user.png'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { logoutApi } from '@/api/login'
|
||||
import { logoutHandler } from '@/utils/logout'
|
||||
import VanButton from 'vant/es/button'
|
||||
import VanCell from 'vant/es/cell'
|
||||
import VanIcon from 'vant/es/icon'
|
||||
import VanImage from 'vant/es/image'
|
||||
import 'vant/es/image/style'
|
||||
import 'vant/es/icon/style'
|
||||
import 'vant/es/cell/style'
|
||||
import 'vant/es/button/style'
|
||||
const userStore = useUserStoreWithOut()
|
||||
const { push } = useRouter()
|
||||
|
||||
const logout = async () => {
|
||||
await logoutApi()
|
||||
logoutHandler()
|
||||
push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="de-mobile-user">
|
||||
<div class="mobile-user-top">
|
||||
<van-image width="75" height="75" :src="userImg" />
|
||||
<div class="user-name">
|
||||
{{ userStore.name }}
|
||||
</div>
|
||||
</div>
|
||||
<van-cell value="系统信息" is-link>
|
||||
<template #title>
|
||||
<van-icon name="user-o" class="search-icon" />
|
||||
<span class="custom-title">关于</span>
|
||||
</template>
|
||||
</van-cell>
|
||||
<div style="margin: 16px">
|
||||
<van-button round block type="primary" @click="logout"> 注销 </van-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.de-mobile-user {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
.mobile-user-top {
|
||||
padding: 24px 24px 16px 24px;
|
||||
display: flex;
|
||||
|
||||
.user-name {
|
||||
height: 45px;
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-cell__title) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-title {
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 26px;
|
||||
line-height: inherit;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -263,12 +263,7 @@ const handleNodeClick = (data: BusiTreeNode) => {
|
||||
}
|
||||
|
||||
const editorDataset = () => {
|
||||
router.push({
|
||||
path: '/dataset-form',
|
||||
query: {
|
||||
id: nodeInfo.id
|
||||
}
|
||||
})
|
||||
handleEdit(nodeInfo.id)
|
||||
}
|
||||
|
||||
const handleEdit = id => {
|
||||
|
Loading…
Reference in New Issue
Block a user