feat: 可视化大屏

master
LCJ-MinYa 11 months ago
parent 8adb8fd3bd
commit 4874a9abdc

@ -28,6 +28,7 @@
"@vueuse/motion": "^2.2.3", "@vueuse/motion": "^2.2.3",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.7.4", "axios": "^1.7.4",
"countup.js": "^2.8.0",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"element-plus": "^2.8.0", "element-plus": "^2.8.0",

@ -29,6 +29,9 @@ importers:
axios: axios:
specifier: ^1.7.4 specifier: ^1.7.4
version: 1.7.4 version: 1.7.4
countup.js:
specifier: ^2.8.0
version: 2.8.0
dayjs: dayjs:
specifier: ^1.11.12 specifier: ^1.11.12
version: 1.11.12 version: 1.11.12
@ -1416,6 +1419,9 @@ packages:
typescript: typescript:
optional: true optional: true
countup.js@2.8.0:
resolution: {integrity: sha512-f7xEhX0awl4NOElHulrl4XRfKoNH3rB+qfNSZZyjSZhaAoUk6elvhH+MNxMmlmuUJ2/QNTWPSA7U4mNtIAKljQ==}
cross-spawn@7.0.3: cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -4779,6 +4785,8 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.5.4 typescript: 5.5.4
countup.js@2.8.0: {}
cross-spawn@7.0.3: cross-spawn@7.0.3:
dependencies: dependencies:
path-key: 3.1.1 path-key: 3.1.1

@ -1,30 +1,40 @@
const Layout = () => import("@/layout/index.vue"); const Layout = () => import('@/layout/index.vue');
export default [ export default [
{ {
path: "/login", path: '/login',
name: "Login", name: 'Login',
component: () => import("@/views/login/index.vue"), component: () => import('@/views/login/index.vue'),
meta: { meta: {
title: "登录", title: '登录',
showLink: false, showLink: false,
rank: 101 rank: 101,
} },
}, },
{ {
path: "/redirect", path: '/redirect',
component: Layout, component: Layout,
meta: { meta: {
title: "加载中...", title: '加载中...',
showLink: false, showLink: false,
rank: 102 rank: 102,
}, },
children: [ children: [
{ {
path: "/redirect/:path(.*)", path: '/redirect/:path(.*)',
name: "Redirect", name: 'Redirect',
component: () => import("@/layout/redirect.vue") component: () => import('@/layout/redirect.vue'),
} },
] ],
} },
{
path: '/screen',
name: 'Screen',
component: () => import('@/views/screen/index.vue'),
meta: {
title: '可视化大屏',
showLink: false,
rank: 103,
},
},
] satisfies Array<RouteConfigsTable>; ] satisfies Array<RouteConfigsTable>;

@ -27,5 +27,15 @@ export default {
title: '布局模版', title: '布局模版',
}, },
}, },
{
path: '/template/screen',
name: 'TemplateScreen',
component: () => import('@/views/template/screen/index.vue'),
meta: {
title: '大屏模版',
frameSrc: 'TemplateScreen',
keepAlive: true,
},
},
], ],
} satisfies RouteConfigsTable; } satisfies RouteConfigsTable;

@ -0,0 +1,146 @@
<script lang="ts">
export type { CountUp as ICountUp, CountUpOptions } from 'countup.js';
export default {
name: 'CountUp',
};
</script>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue';
import { CountUp } from 'countup.js';
import type { CountUpOptions } from 'countup.js';
const props = withDefaults(
defineProps<{
//
endVal: number | string;
//
startVal?: number | string;
// s
duration?: number | string;
//
autoplay?: boolean;
// /
loop?: boolean | number | string | any;
// s
delay?: number;
// countup
options?: CountUpOptions;
}>(),
{
startVal: 0,
duration: 2.5,
autoplay: true,
loop: false,
delay: 0,
options: undefined,
}
);
const emits = defineEmits<{
// countup init complete
(event: 'init', countup: CountUp): void;
// count complete
(event: 'finished'): void;
}>();
let elRef = ref<HTMLElement>();
let countUp = ref<CountUp>();
const initCountUp = () => {
if (!elRef.value) return;
const startVal = Number(props.startVal);
const endVal = Number(props.endVal);
const duration = Number(props.duration);
countUp.value = new CountUp(elRef.value, endVal, {
startVal,
duration,
...props.options,
});
if (countUp.value.error) {
console.error(countUp.value.error);
return;
}
emits('init', countUp.value);
};
const startAnim = (cb?: () => void) => {
countUp.value?.start(cb);
};
// endVal change & autoplay: true, restart animate
watch(
() => props.endVal,
(value) => {
if (props.autoplay) {
countUp.value?.update(value);
}
}
);
// loop animation
const finished = ref(false);
let loopCount = 0;
const loopAnim = () => {
loopCount++;
startAnim(() => {
const isTruely = typeof props.loop === 'boolean' && props.loop;
if (isTruely || props.loop > loopCount) {
delay(() => {
countUp.value?.reset();
loopAnim();
}, props.delay);
} else {
finished.value = true;
}
});
};
watch(finished, (flag) => {
if (flag) {
emits('finished');
}
});
onMounted(() => {
initCountUp();
if (props.autoplay) {
loopAnim();
}
});
onUnmounted(() => {
cancelAnimationFrame(dalayRafId);
countUp.value?.reset();
});
let dalayRafId: number;
// delay to execute callback function
const delay = (cb: () => unknown, seconds = 1) => {
let startTime: number;
function count(timestamp: number) {
if (!startTime) startTime = timestamp;
const diff = timestamp - startTime;
if (diff < seconds * 1000) {
dalayRafId = requestAnimationFrame(count);
} else {
cb();
}
}
dalayRafId = requestAnimationFrame(count);
};
const restart = () => {
initCountUp();
startAnim();
};
defineExpose({
init: initCountUp,
restart,
});
</script>
<template>
<div class="countup-wrap">
<slot name="prefix"></slot>
<span ref="elRef"> </span>
<slot name="suffix"></slot>
</div>
</template>

@ -0,0 +1,3 @@
import CountUp from "./count-up.vue"
export default CountUp

@ -0,0 +1,90 @@
<script setup lang="ts">
import { computed, ref ,onBeforeUpdate, nextTick} from "vue";
import { merge } from "lodash-es";
import { useElementSize } from "@vueuse/core";
import type { PropType } from "vue";
const props = defineProps({
color: {
type: Array as unknown as PropType<[string, string]>,
default: () => [],
},
backgroundColor: {
type: String,
default: "transparent",
},
});
const defaultColor = ["#6586ec", "#2cf7fe"];
const domRef = ref(null);
const { width, height } = useElementSize(domRef,{width:0,height:0}, { box: 'border-box' });
const mergedColor = computed<[string, string]>(() => {
return merge(defaultColor, props.color);
});
</script>
<template>
<div class="dv-border-box-13 dv-border-box" ref="domRef">
<svg :width="width" :height="height" class="dv-border-svg-container">
<path
:fill="backgroundColor"
:stroke="mergedColor[0]"
:d="`
M 5 20 L 5 10 L 12 3 L 60 3 L 68 10
L ${width - 20} 10 L ${width - 5} 25
L ${width - 5} ${height - 5} L 20 ${height - 5}
L 5 ${height - 20} L 5 20
`"
/>
<path
fill="transparent"
stroke-width="3"
stroke-linecap="round"
stroke-dasharray="10, 5"
:stroke="mergedColor[0]"
:d="`M 16 9 L 61 9`"
/>
<path
fill="transparent"
stroke="{mergedColor[1]}"
:d="`M 5 20 L 5 10 L 12 3 L 60 3 L 68 10`"
/>
<path
fill="transparent"
:stroke="mergedColor[1]"
:d="`M ${width - 5} ${height - 30} L ${width - 5} ${height - 5} L ${
width - 30
} ${height - 5}`"
/>
</svg>
<div class="dv-border-box-content">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
.dv-border-box {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100%;
}
.dv-border-svg-container {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
display: block;
}
.dv-border-box-content {
position: relative;
width: 100%;
height: 100%;
}
</style>

@ -0,0 +1,3 @@
import BorderBox13 from "./border-box-13.vue"
export default BorderBox13

@ -0,0 +1,186 @@
<script setup lang="ts">
import { onMounted, reactive, ref, watch } from "vue";
import type { DefaultConfigType } from "./index.d";
import { cloneDeep } from "lodash-es";
import { merge } from "lodash-es";
const mergedConfig = ref<any>(null);
const capsuleLength = ref<any>([]);
const capsuleValue = ref<any>([]);
const labelData = ref<any>([]);
// const labelDataLength = ref<any>([]);
const defaultConfig = reactive<DefaultConfigType>({
// Colors (hex|rgb|rgba|color keywords) ['#000', 'rgb(0, 0, 0)', 'rgba(0, 0, 0, 1)', 'red']
colors: [
"#37a2da",
"#32c5e9",
"#67e0e3",
"#9fe6b8",
"#ffdb5c",
"#ff9f7f",
"#fb7293",
],
unit: "",
showValue: false, // Show item value
});
const props = withDefaults(
defineProps<{
config: object | any;
data: Array<{
name: string;
value: string | number;
}>;
}>(),
{
config: () => { },
data: () => [],
}
);
const calcData = () => {
mergeConfig();
calcCapsuleLengthAndLabelData();
};
const mergeConfig = () => {
mergedConfig.value = merge(cloneDeep(defaultConfig), props.config || {});
};
const calcCapsuleLengthAndLabelData = () => {
if (!props.data.length) return;
const newcapsuleValue = props.data.map((item: any) => item.value);
const maxValue = Math.max(...newcapsuleValue);
capsuleValue.value = newcapsuleValue;
capsuleLength.value = newcapsuleValue.map((v: any) =>
maxValue ? v / maxValue : 0
);
const oneFifth = maxValue / 5;
const newlabelData = Array.from(
new Set(new Array(6).fill(0).map((v, i) => Math.ceil(i * oneFifth)))
);
labelData.value = newlabelData;
// labelDataLength.value = Array.from(newlabelData).map((v) =>
// maxValue ? v / maxValue : 0
// );
// console.log(labelDataLength.value);
};
watch(
() => props.data,
(newval: any) => {
calcData();
},
);
watch(
() => props.config,
(newval: any) => {
calcData();
},
);
onMounted(() => {
calcData();
});
</script>
<template>
<div class="dv-capsule-chart">
<template v-if="mergedConfig">
<div class="label-column">
<div v-for="item in data" :key="item.name">
{{ item.name }}
</div>
<div>&nbsp;</div>
</div>
<div class="capsule-container">
<div class="capsule-item" v-for="(capsule, index) in capsuleLength" :key="index">
<div class="capsule-item-column" :style="`width: ${capsule * 100}%; background-color: ${mergedConfig.colors[index % mergedConfig.colors.length]
};`">
<div v-if="mergedConfig.showValue" class="capsule-item-value">
{{ capsuleValue[index] }}
</div>
</div>
</div>
<div class="unit-label">
<div v-for="(label, index) in labelData" :key="label + index">
{{ label }}
</div>
</div>
</div>
<div class="unit-text" v-if="mergedConfig.unit">
{{ mergedConfig.unit }}
</div>
</template>
</div>
</template>
<style scoped lang="scss">
.dv-capsule-chart {
position: relative;
display: flex;
flex-direction: row;
box-sizing: border-box;
padding: 10px;
color: #fff;
.label-column {
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
padding-right: 10px;
text-align: right;
font-size: 12px;
div {
height: 20px;
line-height: 20px;
}
}
.capsule-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.capsule-item {
box-shadow: 0 0 3px #999;
height: 10px;
margin: 5px 0px;
border-radius: 5px;
.capsule-item-column {
position: relative;
height: 8px;
margin-top: 1px;
border-radius: 5px;
transition: all 0.3s;
display: flex;
justify-content: flex-end;
align-items: center;
.capsule-item-value {
font-size: 12px;
transform: translateX(100%);
}
}
}
.unit-label {
height: 20px;
font-size: 12px;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.unit-text {
text-align: right;
display: flex;
align-items: flex-end;
font-size: 12px;
line-height: 20px;
margin-left: 10px;
}
}
</style>

@ -0,0 +1,6 @@
export interface DefaultConfigType {
colors: Array<String>;
unit:string,
showValue:Boolean
}

@ -0,0 +1,3 @@
import CapsuleChart from "./capsule-chart.vue"
export * from "./index.d"
export default CapsuleChart

@ -0,0 +1,2 @@
import ItemWrap from "./item-wrap.vue"
export default ItemWrap

@ -0,0 +1,77 @@
<script setup lang="ts">
import BorderBox13 from '../datav/border-box-13';
const props = withDefaults(
defineProps<{
//
title: number | string;
}>(),
{
title: '',
}
);
</script>
<template>
<BorderBox13>
<div
class="item_title"
v-if="title !== ''"
>
<div class="zuo"></div>
<span class="title-inner"> &nbsp;&nbsp;{{ title }}&nbsp;&nbsp; </span>
<div class="you"></div>
</div>
<div :class="title !== '' ? 'item_title_content' : 'item_title_content_def'">
<slot></slot></div
></BorderBox13>
</template>
<style scoped lang="scss">
$item-title-height: 38px;
$item_title_content-height: calc(100% - 38px);
.item_title {
height: $item-title-height;
line-height: $item-title-height;
width: 100%;
color: #31abe3;
text-align: center;
// background: linear-gradient(to right, transparent, #0f0756, transparent);
position: relative;
display: flex;
align-items: center;
justify-content: center;
.zuo,
.you {
width: 58px;
height: 14px;
background-image: url('@/assets/img/titles/zuo.png');
}
.you {
transform: rotate(180deg);
}
.title-inner {
font-weight: 900;
letter-spacing: 2px;
background: linear-gradient(92deg, #0072ff 0%, #00eaff 48.8525390625%, #01aaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
:deep(.dv-border-box-content) {
box-sizing: border-box;
padding: 6px 16px 0px;
}
.item_title_content {
height: $item_title_content-height;
}
.item_title_content_def {
width: 100%;
height: 100%;
}
</style>

@ -0,0 +1,3 @@
import ScaleScreen from './scale-screen.vue'
export default ScaleScreen

@ -0,0 +1,247 @@
<template>
<section
:style="{ ...styles.box, ...boxStyle }"
class="v-screen-box"
ref="box"
>
<div
:style="{ ...styles.wrapper, ...wrapperStyle }"
class="screen-wrapper"
ref="screenWrapper"
>
<slot></slot>
</div>
</section>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import type { CSSProperties, PropType } from 'vue';
/**
* 防抖函数
* @param {Function} fn
* @param {number} delay
* @returns {() => void}
*/
function debounce(fn: Function, delay: number): () => void {
// let timer: NodeJS.Timer;
let timer: any;
return function (...args: any[]): void {
if (timer) clearTimeout(timer);
timer = setTimeout(
() => {
typeof fn === 'function' && fn.apply(null, args);
clearTimeout(timer);
},
delay > 0 ? delay : 100
);
};
}
interface IState {
originalWidth: string | number;
originalHeight: string | number;
width?: string | number;
height?: string | number;
observer: null | MutationObserver;
}
type IAutoScale =
| any
| boolean
| {
x?: boolean;
y?: boolean;
};
const props = defineProps({
width: {
type: [String, Number] as PropType<string | number>,
default: 1920,
},
height: {
type: [String, Number] as PropType<string | number>,
default: 1080,
},
fullScreen: {
type: Boolean as PropType<boolean>,
default: false,
},
autoScale: {
type: [Object, Boolean] as PropType<IAutoScale>,
default: true,
},
delay: {
type: Number as PropType<number>,
default: 500,
},
boxStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({}),
},
wrapperStyle: {
type: Object as PropType<CSSProperties>,
default: () => ({}),
},
});
const state = reactive<IState>({
width: 0,
height: 0,
originalWidth: 0,
originalHeight: 0,
observer: null,
});
const styles: Record<string, CSSProperties> = {
box: {
overflow: 'hidden',
backgroundSize: `100% 100%`,
background: `#000`,
width: `100vw`,
height: `100vh`,
},
wrapper: {
transitionProperty: `all`,
transitionTimingFunction: `cubic-bezier(0.4, 0, 0.2, 1)`,
transitionDuration: `500ms`,
position: `relative`,
overflow: `hidden`,
zIndex: 100,
transformOrigin: `left top`,
},
};
const screenWrapper = ref<HTMLElement>();
const box = ref<HTMLElement>();
watch(
() => props.autoScale,
async (newVal: any) => {
if (newVal) {
onResize();
addListener();
} else {
clearListener();
clearScreenWrapperStyle();
}
}
);
/**
* 初始化大屏容器宽高
*/
const initSize = () => {
return new Promise<void>((resolve) => {
box.value!.scrollLeft = 0;
box.value!.scrollTop = 0;
nextTick(() => {
// region
if (props.width && props.height) {
state.width = props.width;
state.height = props.height;
} else {
state.width = screenWrapper.value?.clientWidth;
state.height = screenWrapper.value?.clientHeight;
}
// endregion
// region
if (!state.originalHeight || !state.originalWidth) {
state.originalWidth = window.screen.width;
state.originalHeight = window.screen.height;
}
// endregion
resolve();
});
});
};
/**
* 更新大屏容器宽高
*/
const updateSize = () => {
if (state.width && state.height) {
screenWrapper.value!.style.width = `${state.width}px`;
screenWrapper.value!.style.height = `${state.height}px`;
} else {
screenWrapper.value!.style.width = `${state.originalWidth}px`;
screenWrapper.value!.style.height = `${state.originalHeight}px`;
}
};
const clearScreenWrapperStyle = () => {
screenWrapper.value!.style.transform = '';
screenWrapper.value!.style.margin = '';
};
const autoScale = (scale: number) => {
if (!props.autoScale) {
return;
}
const domWidth = screenWrapper.value!.clientWidth;
const domHeight = screenWrapper.value!.clientHeight;
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
screenWrapper.value!.style.transform = `scale(${scale},${scale})`;
let mx = Math.max((currentWidth - domWidth * scale) / 2, 0);
let my = Math.max((currentHeight - domHeight * scale) / 2, 0);
if (props.autoScale && typeof props.autoScale === 'object') {
!props.autoScale.x && (mx = 0);
!props.autoScale.y && (my = 0);
}
screenWrapper.value!.style.margin = `${my}px ${mx}px`;
};
const updateScale = () => {
//
const currentWidth = document.body.clientWidth;
const currentHeight = document.body.clientHeight;
//
const realWidth = state.width || state.originalWidth;
const realHeight = state.height || state.originalHeight;
//
const widthScale = currentWidth / +realWidth;
const heightScale = currentHeight / +realHeight;
//
if (props.fullScreen) {
screenWrapper.value!.style.transform = `scale(${widthScale},${heightScale})`;
return false;
}
//
const scale = Math.min(widthScale, heightScale);
autoScale(scale);
};
const onResize = debounce(async () => {
await initSize();
updateSize();
updateScale();
}, props.delay);
const initMutationObserver = () => {
const observer = (state.observer = new MutationObserver(() => {
onResize();
}));
observer.observe(screenWrapper.value!, {
attributes: true,
attributeFilter: ['style'],
attributeOldValue: true,
});
};
const clearListener = () => {
window.removeEventListener('resize', onResize);
// state.observer?.disconnect();
};
const addListener = () => {
window.addEventListener('resize', onResize);
// initMutationObserver();
};
onMounted(() => {
nextTick(async () => {
await initSize();
updateSize();
updateScale();
addListener();
// initMutationObserver();
});
});
onUnmounted(() => {
clearListener();
// state.observer?.disconnect();
});
</script>

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineComponent, onBeforeMount, onMounted, ref, watch, nextTick } from 'vue'; import { computed, onBeforeMount, onMounted, ref, watch, nextTick } from 'vue';
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
type propsType = { type propsType = {

@ -0,0 +1,193 @@
<template>
<div class="d-flex jc-center title_wrap">
<div class="zuojuxing"></div>
<div class="youjuxing"></div>
<div class="guang"></div>
<div class="d-flex jc-center">
<div class="title">
<span class="title-text">{{ currentOrg.name }}可视化大屏</span>
<IconifyIconOffline
:style="{ transform: `rotate(${ratation}deg)` }"
class="select-icon"
width="30px"
height="30px"
:icon="CaretRight"
@click="toggleShowSelectOrg"
/>
<ul :class="['select-ul', { fade: isShow }]">
<li
v-for="item in orgList"
:key="item.id"
:class="{ selected: item.id === currentOrg.id }"
@click="toggleSelectOrg(item)"
>
{{ item.name }}
</li>
</ul>
</div>
</div>
<div class="timers">
<span> {{ dateData.dateYear }} {{ dateData.dateWeek }} {{ dateData.dateDay }}</span>
<LaySidebarFullScreen id="full-screen" />
</div>
</div>
</template>
<script setup lang="jsx">
import { ref, reactive, inject, computed } from 'vue';
import dayjs from 'dayjs';
import LaySidebarFullScreen from '@/layout/components/lay-sidebar/components/SidebarFullScreen.vue';
import CaretRight from '@iconify-icons/ep/caret-right';
const props = defineProps({
orgList: {
type: Array,
default: () => [],
},
});
const currentOrg = inject('currentOrg');
const emits = defineEmits(['changeOrg']);
const isShow = ref(false);
const toggleShowSelectOrg = () => {
isShow.value = !isShow.value;
};
const toggleSelectOrg = (item) => {
isShow.value = false;
emits('changeOrg', item);
};
const ratation = computed(() => (isShow.value ? 90 : 0));
const weekday = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const dateData = reactive({
dateDay: '',
dateYear: '',
dateWeek: '',
timing: null,
});
const timeFn = () => {
dateData.timing = setInterval(() => {
dateData.dateDay = dayjs().format('YYYY-MM-DD HH:mm:ss');
dateData.dateWeek = weekday[dayjs().day()];
}, 1000);
};
timeFn();
</script>
<style scoped lang="scss">
.title_wrap {
height: 60px;
background-image: url('../img/headers/top.png');
background-size: cover;
background-position: center center;
position: relative;
margin-bottom: 4px;
.guang {
position: absolute;
bottom: -26px;
background-image: url('../img/headers/guang.png');
background-position: 80px center;
width: 100%;
height: 56px;
}
.zuojuxing,
.youjuxing {
position: absolute;
top: -2px;
width: 140px;
height: 6px;
background-image: url('../img/headers/juxing1.png');
}
.zuojuxing {
left: 11%;
}
.youjuxing {
right: 11%;
transform: rotate(180deg);
}
.timers {
position: absolute;
right: 0;
top: 30px;
font-size: 18px;
display: flex;
align-items: center;
color: #fff;
& > span:first-child {
line-height: 1;
padding-right: 10px;
}
.fullscreen-icon {
cursor: pointer;
background: transparent;
}
}
}
.title {
position: relative;
// width: 500px;
text-align: center;
background-size: cover;
color: transparent;
height: 60px;
line-height: 46px;
.title-text {
font-size: 38px;
font-weight: 900;
letter-spacing: 6px;
width: 100%;
background: linear-gradient(92deg, #0072ff 0%, #00eaff 48.8525390625%, #01aaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.select-icon {
cursor: pointer;
display: inline-block;
color: #0072ff;
vertical-align: text-bottom;
transition: transform 0.3s ease;
}
.select-ul {
max-height: 0;
opacity: 0;
overflow: hidden;
position: absolute;
color: #f1f1f1;
background: rgb(48, 49, 51);
border-radius: 10px;
width: 150px;
z-index: 999;
left: 50%;
margin-left: 150px;
transition: all 0.3s ease;
padding: 5px 0;
li {
cursor: pointer;
height: 40px;
line-height: 40px;
}
li:hover {
background: rgb(104, 104, 105);
}
li.selected {
background: rgb(104, 104, 105);
}
}
.select-ul.fade {
max-height: 500px;
opacity: 1;
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,61 @@
<template>
<scale-screen
width="1920"
height="1080"
:delay="300"
:full-screen="false"
:box-style="{
background: '#03050c',
overflow: 'hidden',
}"
:wrapper-style="{}"
>
<div class="content_wrap">
<Headers
:orgList="orgList"
@changeOrg="changeOrg"
/>
</div>
</scale-screen>
</template>
<script setup lang="jsx">
import { ref, provide } from 'vue';
import ScaleScreen from './components/common/scale-screen';
import Headers from './components/header.vue';
const orgList = ref([
{
id: 'all',
name: '全局',
},
{
id: 'org1',
name: '机构一',
},
{
id: 'org2',
name: '机构二',
},
]);
const currentOrg = ref({
id: 'all',
name: '全局',
});
provide('currentOrg', currentOrg);
const changeOrg = (item) => {
currentOrg.value = item;
};
</script>
<style lang="scss" scoped>
.content_wrap {
width: 100%;
height: 100%;
padding: 16px;
box-sizing: border-box;
background-image: url('./img/pageBg.png');
background-size: cover;
background-position: center center;
}
</style>

@ -0,0 +1,19 @@
<template>
<iframe
ref="iframe"
:src="src"
frameborder="0"
width="100%"
height="100%"
/>
</template>
<script setup lang="jsx">
import { ref } from 'vue';
defineOptions({
name: 'TemplateScreen',
});
const origin = window.location.origin;
const src = ref(`${origin}/#/screen`);
</script>
Loading…
Cancel
Save