提交 9c0c3e2a authored 作者: lidongxu's avatar lidongxu

Merge branch 'ceshi_quickbi'

<template> <template>
<template v-if="!isInWhiteList"> <template v-if="shouldShowWatermark">
<el-watermark :font="font" <el-watermark :font="font"
:content="content" :content="content"
class="wm-class"> class="wm-class"
:class="{'wm-mobile-class': isMobile}">
<router-view /> <router-view />
</el-watermark> </el-watermark>
</template> </template>
...@@ -16,13 +17,30 @@ import useSettingsStore from '@/store/modules/settings' ...@@ -16,13 +17,30 @@ import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme' import { handleThemeStyle } from '@/utils/theme'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import { isWhiteList } from '@/permission' import { isWhiteList } from '@/permission'
import { isMobile } from '@/utils'
const route = useRoute() const route = useRoute()
/*************** 水印相关 *************** */
const isInWhiteList = computed(() => isWhiteList(route.path)) const isInWhiteList = computed(() => isWhiteList(route.path))
const isInFeishu = computed(() => {
const userAgent = navigator.userAgent.toLowerCase()
// 检测飞书用户代理标识
return userAgent.includes('lark') || userAgent.includes('feishu')
})
const shouldShowWatermark = computed(() => {
// 既不在飞书也不在白名单则显示水印
return !isInWhiteList.value && !isInFeishu.value
})
// 水印显示内容
const content = computed(() => { const content = computed(() => {
return useUserStore().userInfo.nickName + ' ' + useUserStore().userInfo.phonenumber?.substring(7) return useUserStore().userInfo.nickName + ' ' + useUserStore().userInfo.phonenumber?.substring(7)
}) })
// 水印字体,暗黑模式切换水印文字颜色
const isDark = computed(() => { const isDark = computed(() => {
return useSettingsStore().isDark return useSettingsStore().isDark
}) })
...@@ -51,8 +69,13 @@ onMounted(() => { ...@@ -51,8 +69,13 @@ onMounted(() => {
<style scoped <style scoped
lang="scss"> lang="scss">
.wm-class{ .wm-class {
min-height: 100%; min-height: 100%;
height: 100%; height: 100%;
} }
.wm-mobile-class{
min-height: 100%;
height: auto;
}
</style> </style>
...@@ -30,6 +30,7 @@ export * from './promotion/display_schedule' ...@@ -30,6 +30,7 @@ export * from './promotion/display_schedule'
export * from './promotion/display_schedule_dashboard' export * from './promotion/display_schedule_dashboard'
export * from './promotion/plan' export * from './promotion/plan'
export * from './promotion/task' export * from './promotion/task'
export * from './quickbi'
export * from './scm/logistics_receipt' export * from './scm/logistics_receipt'
export * from './system/dict/data' export * from './system/dict/data'
export * from './system/dict/type' export * from './system/dict/type'
......
import request from '@/utils/request'
// 获取 quickBI 列表
export const getQuickbiList = (params) => {
return request({
url: '/report/extra/quickbi/embed/list',
params
})
}
// 获取 ticketId
export const getTicketId = (params) => {
return request({
url: '/report/extra/quickbi/embed/accessTicket',
params
})
}
\ No newline at end of file
...@@ -101,6 +101,7 @@ const rightChange = (val) => { ...@@ -101,6 +101,7 @@ const rightChange = (val) => {
height: 100%; height: 100%;
.el-card__header { .el-card__header {
font-size: 18px;
.custom { .custom {
float: right; float: right;
padding: 3px 0; padding: 3px 0;
......
...@@ -77,7 +77,7 @@ const getLogoTextColor = computed(() => { ...@@ -77,7 +77,7 @@ const getLogoTextColor = computed(() => {
// width: 32px; // width: 32px;
// height: 32px; // height: 32px;
width: 120px; width: 120px;
height: 60px; height: 68px;
vertical-align: middle; vertical-align: middle;
// margin-right: 12px; // margin-right: 12px;
} }
......
...@@ -104,7 +104,7 @@ defineExpose({ ...@@ -104,7 +104,7 @@ defineExpose({
} }
:deep(.el-scrollbar__wrap) { :deep(.el-scrollbar__wrap) {
height: 34px !important; // height: 34px !important;
/* 与tags-view-container高度保持一致 */ /* 与tags-view-container高度保持一致 */
overflow-y: hidden !important; overflow-y: hidden !important;
/* 明确禁用垂直滚动 */ /* 明确禁用垂直滚动 */
...@@ -120,6 +120,11 @@ defineExpose({ ...@@ -120,6 +120,11 @@ defineExpose({
:deep(.el-scrollbar__wrap) { :deep(.el-scrollbar__wrap) {
height: 39px; height: 39px;
display: flex;
align-items: center;
.el-scrollbar__view{
display: flex;
}
} }
} }
</style> </style>
\ No newline at end of file
<template> <template>
<div id="tags-view-container" class="tags-view-container"> <div id="tags-view-container"
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll"> class="tags-view-container">
<router-link <scroll-pane ref="scrollPaneRef"
v-for="tag in visitedViews" class="tags-view-wrapper"
@scroll="handleScroll">
<router-link v-for="tag in visitedViews"
:key="tag.path" :key="tag.path"
:data-path="tag.path" :data-path="tag.path"
:class="isActive(tag) ? 'active' : ''" :class="isActive(tag) ? 'active' : ''"
...@@ -10,28 +12,34 @@ ...@@ -10,28 +12,34 @@
class="tags-view-item" class="tags-view-item"
:style="activeStyle(tag)" :style="activeStyle(tag)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)" @contextmenu.prevent="openMenu(tag, $event)">
>
{{ tag.title }} {{ tag.title }}
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)"> <span v-if="!isAffix(tag)"
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" /> @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close"
style="width: 1em; height: 1em;vertical-align: middle;" />
</span> </span>
</router-link> </router-link>
</scroll-pane> </scroll-pane>
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> <ul v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)"> <li @click="refreshSelectedTag(selectedTag)">
<refresh-right style="width: 1em; height: 1em;" /> 刷新页面 <refresh-right style="width: 1em; height: 1em;" /> 刷新页面
</li> </li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"> <li v-if="!isAffix(selectedTag)"
@click="closeSelectedTag(selectedTag)">
<close style="width: 1em; height: 1em;" /> 关闭当前 <close style="width: 1em; height: 1em;" /> 关闭当前
</li> </li>
<li @click="closeOthersTags"> <li @click="closeOthersTags">
<circle-close style="width: 1em; height: 1em;" /> 关闭其他 <circle-close style="width: 1em; height: 1em;" /> 关闭其他
</li> </li>
<li v-if="!isFirstView()" @click="closeLeftTags"> <li v-if="!isFirstView()"
@click="closeLeftTags">
<back style="width: 1em; height: 1em;" /> 关闭左侧 <back style="width: 1em; height: 1em;" /> 关闭左侧
</li> </li>
<li v-if="!isLastView()" @click="closeRightTags"> <li v-if="!isLastView()"
@click="closeRightTags">
<right style="width: 1em; height: 1em;" /> 关闭右侧 <right style="width: 1em; height: 1em;" /> 关闭右侧
</li> </li>
<li @click="closeAllTags(selectedTag)"> <li @click="closeAllTags(selectedTag)">
...@@ -141,7 +149,7 @@ function initTags() { ...@@ -141,7 +149,7 @@ function initTags() {
for (const tag of res) { for (const tag of res) {
// Must have tag name // Must have tag name
if (tag.name) { if (tag.name) {
useTagsViewStore().addVisitedView(tag) useTagsViewStore().addVisitedView(tag)
} }
} }
} }
...@@ -257,81 +265,105 @@ function handleScroll() { ...@@ -257,81 +265,105 @@ function handleScroll() {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss"
.tags-view-container { scoped>
height: 34px; .tags-view-container {
width: 100%; height: 34px;
background: var(--tags-bg, #fff); width: 100%;
border-bottom: 1px solid var(--tags-item-border, #d8dce5); background: var(--tags-bg, #fff);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04); border-bottom: 1px solid var(--tags-item-border, #d8dce5);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.tags-view-wrapper {
.tags-view-item { // 添加移动端适配
display: inline-block; @media (max-width: 768px) {
position: relative; height: 40px; // 移动端适当增加高度
cursor: pointer;
height: 26px; .tags-view-wrapper {
line-height: 26px; .tags-view-item {
border: 1px solid var(--tags-item-border, #d8dce5); height: 32px;
color: var(--tags-item-text, #495060); line-height: 32px;
background: var(--tags-item-bg, #fff); font-size: 13px;
padding: 0 8px; padding: 0 6px;
font-size: 12px; margin-top: 4px;
margin-left: 5px;
margin-top: 4px; &:first-of-type {
margin-left: 10px;
&:first-of-type { }
margin-left: 15px;
&:last-of-type {
margin-right: 10px;
}
}
} }
}
&:last-of-type { .tags-view-wrapper {
margin-right: 15px; .tags-view-item {
} display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid var(--tags-item-border, #d8dce5);
color: var(--tags-item-text, #495060);
background: var(--tags-item-bg, #fff);
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&.active { &:last-of-type {
background-color: #42b983; margin-right: 15px;
color: #fff; }
border-color: #42b983;
&.active {
&::before { background-color: #42b983;
content: ''; color: #fff;
background: #fff; border-color: #42b983;
display: inline-block;
width: 8px; &::before {
height: 8px; content: '';
border-radius: 50%; background: #fff;
position: relative; display: inline-block;
margin-right: 5px; width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 5px;
}
} }
} }
} }
}
.contextmenu { .contextmenu {
margin: 0;
background: var(--el-bg-color-overlay, #fff);
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: var(--tags-item-text, #333);
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
border: 1px solid var(--el-border-color-light, #e4e7ed);
li {
margin: 0; margin: 0;
padding: 7px 16px; background: var(--el-bg-color-overlay, #fff);
cursor: pointer; z-index: 3000;
position: absolute;
&:hover { list-style-type: none;
background: var(--tags-item-hover, #eee); padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: var(--tags-item-text, #333);
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
border: 1px solid var(--el-border-color-light, #e4e7ed);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: var(--tags-item-hover, #eee);
}
} }
} }
} }
}
</style> </style>
<style lang="scss"> <style lang="scss">
......
...@@ -106,7 +106,7 @@ function setLayout() { ...@@ -106,7 +106,7 @@ function setLayout() {
transition: width 0.28s; transition: width 0.28s;
} }
.app-main{ .app-main {
// 头部固定菜单高度 50px // 头部固定菜单高度 50px
height: calc(100vh - 50px); height: calc(100vh - 50px);
} }
...@@ -122,6 +122,11 @@ function setLayout() { ...@@ -122,6 +122,11 @@ function setLayout() {
.fixed-header+.app-main { .fixed-header+.app-main {
height: 100vh; height: 100vh;
padding-top: 84px; padding-top: 84px;
// 移动端适配
@media (max-width: 768px) {
padding-top: 90px; // 调整为50px导航栏 + 40px TagsView
}
} }
} }
...@@ -136,6 +141,11 @@ function setLayout() { ...@@ -136,6 +141,11 @@ function setLayout() {
.mobile .fixed-header { .mobile .fixed-header {
width: 100%; width: 100%;
// 确保移动端TagsView正确显示
.tags-view-container {
height: 40px;
}
} }
......
...@@ -2,7 +2,7 @@ import { createWebHashHistory, createRouter, createWebHistory } from 'vue-router ...@@ -2,7 +2,7 @@ import { createWebHashHistory, createRouter, createWebHistory } from 'vue-router
/* Layout */ /* Layout */
import Layout from '@/layout' import Layout from '@/layout'
import { isMobile } from '@/utils' import { isMobile } from '@/utils'
import { constantMobileRoutes } from '@/views/mobile/router/index.js' import { constantMobileRoutes } from '@/views/mobile/router/index.js' // 移动端路由表
/** /**
* Note: 路由配置项 * Note: 路由配置项
...@@ -26,21 +26,42 @@ import { constantMobileRoutes } from '@/views/mobile/router/index.js' ...@@ -26,21 +26,42 @@ import { constantMobileRoutes } from '@/views/mobile/router/index.js'
} }
*/ */
// 使用的公共静态路由 // 使用的公共静态路由
export let constantRoutes = [{ export let constantRoutes = [
path: '/report/jmreport/view/:id', {
component: () => import('@/views/jimureport/entry.vue'), path: '',
hidden: true component: Layout,
}, redirect: '/index',
{ children: [
path: '/report/jmreport/index/:id', {
component: () => import('@/views/jimureport/entry.vue'), path: '/index',
hidden: true component: () => import('@/views/index'),
}, name: 'Index',
{ meta: { title: '首页', icon: 'dashboard', affix: true }
path: '/report/jmreport/shareView/:id', }
component: () => import('@/views/jimureport/entry.vue'), ]
hidden: true },
}] {
path: '/report/jmreport/view/:id',
component: () => import('@/views/jimureport/entry.vue'),
hidden: true
},
{
path: '/report/jmreport/index/:id',
component: () => import('@/views/jimureport/entry.vue'),
hidden: true
},
{
path: '/report/jmreport/shareView/:id',
component: () => import('@/views/jimureport/entry.vue'),
hidden: true
},
// QuickBI 预览
{
path: '/report/quickbi/preview',
component: () => import('@/views/quickbi/preview/index.vue'),
hidden: true
},
]
// PC端 静态路由 // PC端 静态路由
export const constantPCRoutes = [ export const constantPCRoutes = [
...constantRoutes, ...constantRoutes,
...@@ -70,19 +91,7 @@ export const constantPCRoutes = [ ...@@ -70,19 +91,7 @@ export const constantPCRoutes = [
component: () => import('@/views/error/401'), component: () => import('@/views/error/401'),
hidden: true hidden: true
}, },
{
path: '',
component: Layout,
redirect: '/index',
children: [
{
path: '/index',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
{ {
path: '/user', path: '/user',
component: Layout, component: Layout,
......
...@@ -43,6 +43,9 @@ const modules = ref([]); // 替换原有的静态modules ...@@ -43,6 +43,9 @@ const modules = ref([]); // 替换原有的静态modules
// 观察路由挂载后事件 // 观察路由挂载后事件
// 格式化路由为菜单所需结构 // 格式化路由为菜单所需结构
const formatRoutesToModules = (routes) => { const formatRoutesToModules = (routes) => {
// 把首页去除掉
// 把首页去除掉
routes = routes.filter(route => route.path !== '');
// 根据实际路由结构转换,这里假设需要提取一级路由作为模块 // 根据实际路由结构转换,这里假设需要提取一级路由作为模块
modules.value = routes.map(o => { modules.value = routes.map(o => {
return { return {
......
// 移动端 静态路由 // 移动端 静态路由
export const constantMobileRoutes = [ export const constantMobileRoutes = [
{
path: '/index',
redirect: '/menu'
},
{ {
path: '/login', path: '/login',
component: () => import('@/views/login'), component: () => import('@/views/login'),
...@@ -18,7 +14,6 @@ export const constantMobileRoutes = [ ...@@ -18,7 +14,6 @@ export const constantMobileRoutes = [
{ {
path: '/', path: '/',
component: () => import('@/views/mobile/index'), component: () => import('@/views/mobile/index'),
redirect: '/menu',
children: [ children: [
{ {
path: 'menu',// 菜单页 path: 'menu',// 菜单页
......
<template>
<div class="app-container">
<div class="container">
<!-- 表格数据 -->
<el-table v-loading="loading"
:data="reportList">
<el-table-column label="报表 ID"
key="id"
prop="id"
align="left"
width="100"
sortable />
<el-table-column label="报表名称"
key="name"
prop="name"
align="left"
sortable>
<template #default="{ row }">
<div style="display: flex; align-items: center">
<svg-icon icon-class="bg-document"></svg-icon>
<span style="margin-left: 10px">{{ row.name }}</span>
</div>
</template>
</el-table-column>
<!-- 操作预览 -->
<el-table-column label="操作"
key="operation"
align="center">
<template #default="scope">
<el-button text
type="primary"
@click="previewClick(scope)">
预览
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script setup
name="Role">
import { getQuickbiList } from "@/api"
import useUserStore from '@/store/modules/user'
const { proxy } = getCurrentInstance()
const router = useRouter()
const userStore = useUserStore()
const reportBaseDomain = import.meta.env.VITE_APP_REPORT_URL // 基础域名
const reportViewURL = import.meta.env.VITE_APP_REPORT_PREVIEW_URL // 预览
const reportShareViewURL = import.meta.env.VITE_APP_REPORT_SHARE_PREVIEW_URL // 分享预览
const reportEditURL = import.meta.env.VITE_APP_REPORT_EDIT_URL // 新增/编辑
/*************** 报表列表部分 ****************/
// 报表搜索
const queryParams = reactive({
name: undefined, // 报表名称
category: undefined, // 报表类目
pageNum: 1,
pageSize: 10
})
// 获取报表列表
const loading = ref(true)
const reportList = ref([])
// 查询报表列表
function getReportList() {
loading.value = true
getQuickbiList(queryParams).then(response => {
console.log(response)
reportList.value = response.data
loading.value = false
})
}
getReportList()
const previewClick = (scope) => {
const row = scope.row
router.push({
path: `/report/quickbi/preview`,
query: {
id: row.pageId
}
})
}
</script>
<style scoped
lang="scss">
.app-container {
overflow-x: auto;
}
.container {
min-width: 600px;
/* 设置最小宽度确保表格可以横向滚动 */
}
/* 移动端适配 */
@media (max-width: 768px) {
.app-container {
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
/* iOS 平滑滚动 */
}
.container {
min-width: 500px;
/* 移动端需要更大的最小宽度 */
}
/* 表格样式调整 */
::v-deep(.el-table) {
.el-table__body-wrapper {
overflow-x: auto;
}
}
}
</style>
\ No newline at end of file
<template>
<iframe :src="iframeURL"
frameborder="0"
width="100%"
height="800px"></iframe>
</template>
<script setup>
import { useRoute } from 'vue-router'
import { getTicketId } from '@/api'
import { onMounted } from 'vue'
const route = useRoute()
const ticket = ref('')
const iframeURL = ref('')
onMounted(() => {
getTicketId({
pageId: route.query.id
}).then(response => {
ticket.value = response.data.accessTicket
iframeURL.value = `https://bi.aliyun.com/token3rd/dashboard/view/pc.htm?pageId=${route.query.id}&embedDisplayParam=%7B%22showTitle%22%3Afalse%7D&accessTicket=${ticket.value}`
})
})
</script>
<style scoped></style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论