1. 修改登录页面中:星期展示方法
2. 登录页面
3. 首页
This commit is contained in:
Zy
2026-09-28 17:39:06 +08:00
parent 4f7cb2d085
commit a19777a2b8
27 changed files with 920 additions and 500 deletions

View File

@@ -1,42 +1,42 @@
# 页面标题
VITE_APP_TITLE = 智净云 · 智能家居管理后台
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
VITE_APP_TITLE='智净云 · 智能家居管理后台'
VITE_APP_LOGO_TITLE=RuoYi-Vue-Plus
# 开发环境配置
VITE_APP_ENV = 'development'
VITE_APP_ENV='development'
# 开发环境
VITE_APP_BASE_API = '/dev-api'
VITE_APP_BASE_API='/dev-api'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'
VITE_APP_CONTEXT_PATH='/'
# 监控地址
VITE_APP_MONITOR_ADMIN = 'http://localhost:9090/admin/applications'
VITE_APP_MONITOR_ADMIN='http://localhost:9090/admin/applications'
# SnailJob 控制台地址
VITE_APP_SNAILJOB_ADMIN = 'http://localhost:8800/snail-job'
VITE_APP_SNAILJOB_ADMIN='http://localhost:8800/snail-job'
# SnailAI 控制台地址
VITE_APP_SNAILAI_ADMIN = 'http://localhost:8900/snail-ai'
VITE_APP_SNAILAI_ADMIN='http://localhost:8900/snail-ai'
VITE_APP_PORT = 54123
VITE_APP_PORT=5412
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT = true
VITE_APP_ENCRYPT=true
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
VITE_APP_RSA_PUBLIC_KEY='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
VITE_APP_RSA_PRIVATE_KEY='MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
# 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
VITE_APP_CLIENT_ID='e5cd7e4891bf95d1d19206ce24a7b32e'
# 统一消息推送开关
VITE_APP_MESSAGE_ENABLED = true
VITE_APP_MESSAGE_ENABLED=true
# sse / websocket
VITE_APP_MESSAGE_TRANSPORT = 'sse'
VITE_APP_MESSAGE_TRANSPORT='sse'
# 统一消息推送路径
VITE_APP_MESSAGE_PATH = '/resource/message'
VITE_APP_MESSAGE_PATH='/resource/message'

View File

@@ -1,45 +1,45 @@
# 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
VITE_APP_TITLE=RuoYi-Vue-Plus后台管理系统
VITE_APP_LOGO_TITLE=RuoYi-Vue-Plus
# 生产环境配置
VITE_APP_ENV = 'production'
VITE_APP_ENV='production'
# 应用访问路径 例如使用前缀 /admin/
VITE_APP_CONTEXT_PATH = '/'
VITE_APP_CONTEXT_PATH='/'
# 监控地址
VITE_APP_MONITOR_ADMIN = '/admin/applications'
VITE_APP_MONITOR_ADMIN='/admin/applications'
# SnailJob 控制台地址
VITE_APP_SNAILJOB_ADMIN = '/snail-job'
VITE_APP_SNAILJOB_ADMIN='/snail-job'
# SnailAI 控制台地址
VITE_APP_SNAILAI_ADMIN = '/snail-ai'
VITE_APP_SNAILAI_ADMIN='/snail-ai'
# 生产环境
VITE_APP_BASE_API = '/prod-api'
VITE_APP_BASE_API='/prod-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
VITE_BUILD_COMPRESS = gzip
VITE_BUILD_COMPRESS=gzip
VITE_APP_PORT = 80
VITE_APP_PORT=80
# 接口加密功能开关(如需关闭 后端也必须对应关闭)
VITE_APP_ENCRYPT = true
VITE_APP_ENCRYPT=true
# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
VITE_APP_RSA_PUBLIC_KEY='MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvEDuRIOM3oZPWj9Ukoc5pQklR4PFH6/clnjeFqjDLIgDyQvjxhgqAZQA+E9eD6qu6FsXPmK8djcL+nh3cFHz4pX473jDvO3Sve+8yL3VRQ0n2pRgQ2a01MJsy+WwTZCBYWf0VnLRIvANUoWQgy9vz94q7Va44dg7A1/3ICf+xAwIDAQAB'
# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
VITE_APP_RSA_PRIVATE_KEY = 'MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
VITE_APP_RSA_PRIVATE_KEY='MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAObq7yrxfvyieZtTjAYyrdvi59tYTXxjO5ajmPCRSXBY9M9wQ1tli297JN6mnY53UJMNyOFNSZVi8WSFoIXjpR87FmvChJlzeN/dZdd3SEs48Ee66XKeSePYqxa8oO5GKDsnajgpsOHKXSeeVSIysiIPS2/WsEqk0In9P4w3RsRFAgMBAAECgYBiMEWwce24SPICnRzuScBpvmsudrbEDIH7BOd0a6LZlcnLJwZNJ7mJlshPsHNQb+WgEf135+BBGEhioPtn0yuTdEuKP4kB9UdYUKiayWCoWhJpesv7sAD4RDClV7dhuV+gcd1AXD+YzyRIPbGm0VC2U+4q8/+UPRpVjqskbLVTgQJBAPRpou7g3S8n4XB527kq0D8I3+ZYwMxZhszwhrCDpJU319+ucmpLVwYIzDmZVeID2QQdUaDfIEViFHu95xDrGiUCQQDx3YOKn3yaEctk/ERVn7hDAyAXUbd8/pv2b24/M/l1ZevlsFem8U4Jk5Mu64t3z3YGJoymEjQmbucwT01iKhehAkEAxlnccsRmfFh/KkqauKE4M4++NTAd9zlInpUsmZ+cN8UEGnF2RTEzRKBrLOt1uWCqBB7PGiE6DVTVjr7FAQPrSQJAT5yeY87DcONSk9cFlzmPqV8p/QME5rvYEnHzVBKDlkUKNPyqnWToTvaoh9U4fyNmsfeWbEOprszqhFhWHG3GgQJAK8+ynmyFhaw63+Hx2KU5zR4hVuQso2IzrEurGxCxybV6mR7VBerb4502+EPx3PmOgxQL+niUFhcMWxcvBFP9+A=='
# 客户端id
VITE_APP_CLIENT_ID = 'e5cd7e4891bf95d1d19206ce24a7b32e'
VITE_APP_CLIENT_ID='e5cd7e4891bf95d1d19206ce24a7b32e'
# 统一消息推送开关
VITE_APP_MESSAGE_ENABLED = true
VITE_APP_MESSAGE_ENABLED=true
# sse / websocket
VITE_APP_MESSAGE_TRANSPORT = 'sse'
VITE_APP_MESSAGE_TRANSPORT='sse'
# 统一消息推送路径
VITE_APP_MESSAGE_PATH = '/resource/message'
VITE_APP_MESSAGE_PATH='/resource/message'

View File

@@ -53,7 +53,7 @@
& .el-sub-menu {
overflow: hidden;
border-radius: var(--app-radius-md);
//border-radius: var(--app-radius-md);
.el-sub-menu__title,
.el-sub-menu__title.el-tooltip__trigger {

View File

@@ -6,7 +6,7 @@
.sidebar-container {
.theme-dark .submenu-title-noDropdown,
.theme-dark .el-sub-menu__title {
border-radius: var(--app-radius-md);
//border-radius: var(--app-radius-md);
margin: 3px 8px;
height: 40px;
@@ -18,11 +18,10 @@
.submenu-title-noDropdown,
.el-sub-menu__title {
border-radius: var(--app-radius-md);
margin: 3px 8px;
//border-radius: var(--app-radius-md);
margin: 0;
height: 40px;
color: var(--side-menu-text) !important;
&:hover {
background-color: var(--side-menu-hover-bg) !important;
color: var(--side-menu-hover-text) !important;
@@ -36,10 +35,11 @@
& .nest-menu .el-sub-menu > .el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: calc($base-sidebar-width - 16px) !important;
border-radius: var(--app-radius-md);
//border-radius: var(--app-radius-md);
height: 40px;
margin: 3px 8px;
background: transparent !important;
//margin: 3px 8px;
//background: transparent !important;
background: #f7f7f7 !important;
&:not(.is-active):hover {
background-color: var(--side-menu-hover-bg) !important;
@@ -49,11 +49,11 @@
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
border-radius: var(--app-radius-md);
//border-radius: var(--app-radius-md);
height: 40px;
margin: 3px 8px;
background: transparent !important;
//margin: 3px 8px;
//background: transparent !important;
background: #f7f7f7 !important;
&.is-active {
background: var(--side-menu-active-bg) !important;
color: var(--side-menu-active-text) !important;
@@ -70,11 +70,11 @@
& .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
& .theme-dark .el-menu-item {
border-radius: var(--app-radius-md);
//border-radius: var(--app-radius-md);
height: 40px;
margin: 3px 8px;
background: transparent !important;
//margin: 3px 8px;
//background: transparent !important;
background: #f7f7f7 !important;
&.is-active {
background: var(--side-menu-active-bg) !important;
color: var(--side-menu-active-text) !important;
@@ -91,17 +91,17 @@
& .nest-menu .el-sub-menu > .el-sub-menu__title,
& .el-menu-item {
border-radius: var(--app-radius-md);
//border-radius: var(--app-radius-md);
height: 40px;
margin: 3px 8px;
//margin: 0 8px;
background: transparent !important;
&.is-active {
background: var(--side-menu-active-bg) !important;
color: var(--side-menu-active-text) !important;
box-shadow:
inset 3px 0 0 var(--side-menu-active-line),
inset 0 0 0 1px var(--side-menu-active-border);
//box-shadow:
// inset 3px 0 0 var(--side-menu-active-line),
// inset 0 0 0 1px var(--side-menu-active-border);
}
&:not(.is-active):hover {

View File

@@ -26,7 +26,7 @@
position: fixed;
font-size: 0;
top: 12px;
left: 12px;
//left: 12px;
z-index: 1001;
overflow: hidden;
box-sizing: border-box;

View File

@@ -39,4 +39,4 @@ $--color-warning: $color-warning;
$--color-danger: $color-danger;
$--color-info: $color-info;
$base-sidebar-width: 200px;
$base-sidebar-width: 244px;

View File

@@ -2,9 +2,8 @@
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">
{{ item.meta?.title }}
</span>
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"> {{ item.meta?.title }} </span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta?.title }}</a>
</el-breadcrumb-item>
</transition-group>
@@ -37,7 +36,7 @@ const getBreadcrumb = () => {
}
// 判断是否为首页
if (!isDashboard(matched[0])) {
matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched);
// matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched);
}
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false);
};
@@ -82,14 +81,22 @@ onMounted(() => {
display: inline-block;
font-size: 13px;
line-height: 1.2;
margin-left: 4px;
color: var(--app-text-muted);
:deep(.el-breadcrumb__separator) {
margin: 0 3px;
}
:deep(.el-breadcrumb__inner) {
color: inherit;
font-weight: 500;
}
:deep(.el-breadcrumb__item .no-redirect) {
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.no-redirect {
color: var(--app-text-title);
cursor: text;

View File

@@ -0,0 +1,118 @@
<template>
<div ref="chartRef" :style="{ width, height }" class="echarts-container"></div>
</template>
<script lang="ts" setup>
import {ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef} from 'vue';
import * as echarts from 'echarts';
import type {EChartsOption, ECharts} from 'echarts';
interface Props {
width?: string;
height?: string;
options: EChartsOption;
theme?: string;
autoResize?: boolean;
immediate?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
width: '100%',
height: '100%',
theme: '',
autoResize: true,
immediate: true
});
const emit = defineEmits<{
ready: [chart: ECharts];
click: [params: echarts.ECElementEvent];
rendered: [];
}>();
const chartRef = ref<HTMLDivElement>();
// 3. 使用 shallowRef 优化性能,因为 ECharts 实例不需要深层响应式
const chartInstance = shallowRef<ECharts | null>(null);
let resizeObserver: ResizeObserver | null = null;
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
const instance = echarts.init(chartRef.value, props.theme);
chartInstance.value = instance;
chartInstance.value.setOption(props.options);
emit('ready', instance);
emit('rendered');
instance.on('click', (params: any) => {
emit('click', params);
});
};
const updateChart = () => {
if (chartInstance.value) {
chartInstance.value.setOption(props.options, {
notMerge: false,
lazyUpdate: false
});
emit('rendered');
}
};
// ✅ 核心修复:支持 scale 缩放后重绘
const resizeChart = () => {
if (!chartInstance.value) return;
chartInstance.value.resize();
};
const getInstance = () => chartInstance.value;
const exportAsImage = (type: 'png' | 'svg' | 'jpeg' = 'png') => {
return (
chartInstance.value?.getDataURL({
type,
pixelRatio: 2,
backgroundColor: '#fff'
}) || ''
);
};
watch(() => props.options, updateChart, {deep: true});
onMounted(() => {
nextTick(() => initChart());
if (props.autoResize && chartRef.value) {
resizeObserver = new ResizeObserver(resizeChart);
resizeObserver.observe(chartRef.value);
}
window.addEventListener('resize', resizeChart);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeChart);
resizeObserver?.disconnect();
chartInstance.value?.dispose();
chartInstance.value = null;
});
defineExpose({
resizeChart,
updateChart,
getInstance,
exportAsImage
});
</script>
<style lang="scss" scoped>
.echarts-container {
display: block;
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div>
<div class="router_title">{{ route.meta.title }}</div>
<Breadcrumb class="breadcrumb"></Breadcrumb>
</div>
</template>
<script lang="ts" setup>
import Breadcrumb from '@/components/Breadcrumb/index.vue'
const route = useRoute();
</script>
<style lang="scss" scoped>
.router_title {
height: 24px;
line-height: 24px;
border-radius: 0 0 0 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 18px;
}
</style>

View File

@@ -53,7 +53,7 @@ function addIframe() {
width: 100%;
position: relative;
overflow: hidden;
padding: 12px;
//padding: 12px;
&:fullscreen,
&:-webkit-full-screen,

View File

@@ -12,25 +12,17 @@
<router-link v-else-if="showLogo" to="/" class="navtop-logo-shell">
<img :src="appLogo" class="navtop-logo-icon" alt="logo" />
</router-link>
<div class="nav-context">
<breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
<template v-if="navType == NavTypeEnum.TOP">
<top-bar id="topbar-container" class="topbar-container" />
</template>
<breadcrumb v-if="navType == NavTypeEnum.LEFT && isDashboard(route)" id="breadcrumb-container"
class="breadcrumb-container"/>
<!-- <top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />-->
<!-- <template v-if="navType == NavTypeEnum.TOP">-->
<!-- <top-bar id="topbar-container" class="topbar-container" />-->
<!-- </template>-->
</div>
</div>
<div class="right-menu flex align-center">
<template v-if="appStore.device !== 'mobile'">
<search-menu ref="searchMenuRef" />
<el-tooltip content="搜索" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect" @click="openSearchMenu">
<svg-icon class-name="search-icon" icon-class="search" />
</div>
</el-tooltip>
<!-- 消息 -->
<el-tooltip :content="$t('navbar.message')" effect="dark" placement="bottom">
<div>
<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
@@ -47,25 +39,6 @@
</el-popover>
</div>
</el-tooltip>
<el-tooltip content="Github" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="$t('navbar.document')" effect="dark" placement="bottom">
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="$t('navbar.full')" effect="dark" placement="bottom">
<screenfull id="screenfull" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="$t('navbar.language')" effect="dark" placement="bottom">
<lang-select id="lang-select" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip :content="$t('navbar.layoutSize')" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<div class="avatar-container">
<el-dropdown class="avatar-dropdown" trigger="click" @command="handleCommand">
@@ -82,9 +55,9 @@
<router-link to="/user/profile">
<el-dropdown-item>{{ $t('navbar.personalCenter') }}</el-dropdown-item>
</router-link>
<el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">
<span>{{ $t('navbar.layoutSetting') }}</span>
</el-dropdown-item>
<!-- <el-dropdown-item v-if="settingsStore.showSettings" command="setLayout">-->
<!-- <span>{{ $t('navbar.layoutSetting') }}</span>-->
<!-- </el-dropdown-item>-->
<el-dropdown-item divided command="logout">
<span>{{ $t('navbar.logout') }}</span>
</el-dropdown-item>
@@ -110,7 +83,9 @@ import { useUserStore } from '@/store/modules/user';
import notice from './notice/index.vue';
import TopBar from './TopBar/index.vue';
import SearchMenu from './TopBar/search.vue';
import {RouteLocationMatched, RouteLocationNormalizedLoadedGeneric} from "vue-router";
const route = useRoute();
const appStore = useAppStore();
const userStore = useUserStore();
const settingsStore = useSettingsStore();
@@ -120,6 +95,13 @@ const navType = computed(() => settingsStore.navType);
const showLogo = computed(() => settingsStore.sidebarLogo);
const displayName = computed(() => userStore.nickname || '管理员');
const isDashboard = (route: RouteLocationNormalizedLoadedGeneric) => {
const name = route && (route.name as string);
if (!name) {
return false;
}
return name.trim() === 'Index';
};
// 搜索菜单
const searchMenuRef = ref<InstanceType<typeof SearchMenu>>();

View File

@@ -54,7 +54,7 @@ const logoTextColor = computed(() => (isDarkSide.value ? '#f8fbff' : 'var(--app-
position: relative;
flex-shrink: 0;
height: 46px;
line-height: 46px;
//line-height: 46px;
padding: 0 8px;
margin-top: 8px;
background: transparent;
@@ -70,8 +70,8 @@ const logoTextColor = computed(() => (isDarkSide.value ? '#f8fbff' : 'var(--app-
justify-content: center;
gap: 8px;
border-radius: 14px;
background: v-bind(logoSurface);
border: 1px solid v-bind(logoBorder);
//background: v-bind(logoSurface);
//border: 1px solid v-bind(logoBorder);
& .sidebar-logo {
width: 28px;

View File

@@ -9,7 +9,7 @@
>
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
<!-- <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />-->
<template #title>
<span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
</template>

View File

@@ -63,8 +63,8 @@ const menuStyle = computed(() => ({
flex-direction: column;
gap: 8px;
padding: 10px 8px 12px;
border: 1px solid var(--app-sidebar-border);
border-radius: var(--app-radius-base);
//border: 1px solid var(--app-sidebar-border);
//border-radius: var(--app-radius-base);
box-shadow: var(--app-shadow-sm);
background: v-bind(bgColor) !important;
overflow: hidden;

View File

@@ -86,7 +86,8 @@ const setLayout = () => {
position: relative;
height: 100%;
width: 100%;
background: var(--app-shell-bg);
//background: var(--app-shell-bg);
background: #fff;
&.mobile.openSidebar {
position: fixed;
@@ -110,7 +111,7 @@ const setLayout = () => {
display: flex;
flex-direction: column;
gap: 3px;
padding: 12px 12px 0;
//padding: 12px 12px 0;
background: transparent;
}

View File

@@ -69,7 +69,7 @@ export const constantRoutes: RouteRecordRaw[] = [
children: [
{
path: '/index',
component: () => import('@/views/index.vue'),
component: () => import('@/views/index/index.vue'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}

View File

@@ -12,7 +12,7 @@ const setting: DefaultSettings = {
/**
* 侧边栏主题 深色主题theme-dark,浅色主题theme-light
*/
sideTheme: 'theme-dark',
sideTheme: 'theme-light',
/**
* 是否系统布局配置
*/
@@ -26,7 +26,7 @@ const setting: DefaultSettings = {
/**
* 是否显示 tagsView
*/
tagsView: true,
tagsView: false,
/**
* 持久化标签页
@@ -36,12 +36,12 @@ const setting: DefaultSettings = {
/**
* 显示页签图标
*/
tagsIcon: true,
tagsIcon: false,
/**
* 是否固定头部
*/
fixedHeader: true,
fixedHeader: false,
/**
* 是否显示logo

View File

@@ -1,370 +0,0 @@
<template>
<div class="home">
<section class="hero-panel">
<div class="hero-copy">
<h1>RuoYi-Vue-Plus 控制台</h1>
<p>
企业级后台管理系统 重写 RuoYi-Vue 所有功能 集成 Sa-Token、Mybatis-Plus、WarmFlow、SpringDoc、Hutool、OSS
等组件
</p>
<div class="hero-actions">
<el-button type="primary" @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">查看源码</el-button>
<el-button plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')">
更新日志
</el-button>
</div>
</div>
</section>
<div class="content-grid">
<section class="section-card">
<div class="section-head">
<div>
<h2>项目矩阵</h2>
</div>
</div>
<div class="product-list">
<article v-for="product in products" :key="product.name" class="product-card">
<div class="product-top">
<div>
<h3>{{ product.name }}</h3>
<p>{{ product.summary }}</p>
</div>
<span class="product-version">{{ product.version }}</span>
</div>
<div class="product-tags">
<el-tag v-for="tag in product.tags" :key="tag" effect="plain">{{ tag }}</el-tag>
</div>
<div class="product-actions">
<el-button type="primary" plain @click="goTarget(product.primaryUrl)">
{{ product.primaryLabel }}
</el-button>
<el-button plain @click="goTarget(product.secondaryUrl)">{{ product.secondaryLabel }}</el-button>
</div>
</article>
</div>
</section>
<section class="section-card capability-card">
<div class="section-head">
<div>
<h2>能力地图</h2>
</div>
</div>
<div class="capability-groups">
<article v-for="group in capabilityGroups" :key="group.title" class="capability-group">
<h3>{{ group.title }}</h3>
<ul>
<li v-for="item in group.items" :key="item">{{ item }}</li>
</ul>
</article>
</div>
</section>
</div>
</div>
</template>
<script setup name="Index" lang="ts">
const products = [
{
name: 'RuoYi-Vue-Plus',
version: 'v6.0.0',
summary: '面向分布式集群场景的后台管理系统,保持现有业务接口与权限逻辑,适合先完成前端壳升级。',
tags: ['Vue 3', 'Element Plus', 'Spring Boot', 'Sa-Token'],
primaryLabel: '访问 GitHub',
primaryUrl: 'https://github.com/dromara/RuoYi-Vue-Plus',
secondaryLabel: '查看更新日志',
secondaryUrl: 'https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog'
},
{
name: 'RuoYi-Cloud-Plus',
version: 'v6.0.0',
summary: '微服务通用权限管理系统,适合更复杂的服务治理场景,也可以沿用同样的前端升级思路。',
tags: ['Spring Cloud', 'Gateway', 'Nacos', 'Dubbo'],
primaryLabel: '访问 GitHub',
primaryUrl: 'https://github.com/dromara/RuoYi-Cloud-Plus',
secondaryLabel: '查看更新日志',
secondaryUrl: 'https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog'
}
];
const capabilityGroups = [
{
title: '后端基建',
items: ['Spring Boot / Spring Cloud', 'Sa-Token 认证与权限', 'MySQL / Redis', '代码生成器']
},
{
title: '平台能力',
items: ['动态菜单与按钮权限', '监控、日志、在线用户', '任务调度与工作流', '文件存储与多云适配']
},
{
title: '前端方向',
items: ['UI 卡片化', '主题与布局统一', '通用页面容器规范化', '企业化布局']
}
];
const goTarget = (url: string) => {
window.open(url, '__blank');
};
</script>
<style lang="scss" scoped>
.home {
display: flex;
flex-direction: column;
gap: 20px;
}
.hero-panel,
.section-card {
border-radius: 28px;
border: 1px solid var(--app-surface-border);
background: var(--app-surface-bg);
box-shadow: var(--app-shadow-sm);
backdrop-filter: blur(18px);
}
.hero-panel {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.7fr);
gap: 18px;
padding: 30px;
background: radial-gradient(circle at top left, rgba(53, 109, 255, 0.16), transparent 30%), var(--app-surface-bg);
}
.hero-copy {
display: flex;
flex-direction: column;
gap: 14px;
h1 {
margin: 0;
font-size: clamp(30px, 4vw, 46px);
line-height: 1.06;
letter-spacing: -0.04em;
color: var(--app-text-title);
}
p {
margin: 0;
max-width: 760px;
color: var(--app-text-muted);
font-size: 15px;
line-height: 1.8;
}
}
.hero-badge {
display: inline-flex;
width: fit-content;
padding: 8px 14px;
border-radius: 999px;
background: rgba(53, 109, 255, 0.12);
color: var(--app-accent-strong);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 8px;
}
.hero-stats {
display: grid;
gap: 12px;
}
.stat-card {
padding: 18px 20px;
border-radius: 22px;
background: var(--app-elevated-soft-bg);
border: 1px solid var(--app-surface-border);
display: flex;
flex-direction: column;
gap: 6px;
strong {
color: var(--app-text-title);
font-size: 24px;
letter-spacing: -0.03em;
}
span {
color: var(--app-text-muted);
font-size: 13px;
}
}
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr);
gap: 20px;
}
.section-card {
padding: 24px;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
h2 {
margin: 6px 0 0;
color: var(--app-text-title);
font-size: 26px;
letter-spacing: -0.03em;
}
}
.section-kicker {
color: var(--app-text-muted);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.product-list,
.capability-groups {
display: grid;
gap: 16px;
}
.product-card {
padding: 22px;
border-radius: 24px;
background: var(--app-elevated-soft-bg);
border: 1px solid var(--app-surface-border);
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
border-color 0.25s ease;
&:hover {
transform: translateY(-2px);
box-shadow: var(--app-shadow-sm);
border-color: rgba(53, 109, 255, 0.2);
}
}
.product-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
h3 {
margin: 0 0 8px;
color: var(--app-text-title);
font-size: 22px;
letter-spacing: -0.03em;
}
p {
margin: 0;
color: var(--app-text-muted);
line-height: 1.8;
}
}
.product-version {
flex-shrink: 0;
padding: 8px 12px;
border-radius: 999px;
background: rgba(53, 109, 255, 0.12);
color: var(--app-accent-strong);
font-size: 12px;
font-weight: 700;
}
.product-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 18px;
}
.product-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
.capability-card {
background: radial-gradient(circle at top right, rgba(14, 165, 233, 0.12), transparent 28%), var(--app-surface-bg);
}
.capability-group {
padding: 18px 18px 18px 20px;
border-radius: 22px;
background: var(--app-elevated-soft-bg);
border: 1px solid var(--app-surface-border);
h3 {
margin: 0 0 12px;
color: var(--app-text-title);
font-size: 18px;
}
ul {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 10px;
}
li {
position: relative;
padding-left: 16px;
color: var(--app-text-muted);
line-height: 1.7;
&::before {
content: '';
position: absolute;
left: 0;
top: 10px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--app-accent-strong);
box-shadow: 0 0 0 5px rgba(53, 109, 255, 0.12);
}
}
}
@media (max-width: 960px) {
.hero-panel,
.content-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.hero-panel,
.section-card {
padding: 20px;
border-radius: 22px;
}
.product-top {
flex-direction: column;
}
}
html.dark {
.hero-panel {
background: radial-gradient(circle at top left, rgba(53, 109, 255, 0.18), transparent 30%), var(--app-surface-bg);
}
}
</style>

View File

@@ -0,0 +1,99 @@
<script lang="ts" name="activeDevices" setup>
import Echarts from '@/components/Echarts/index.vue';
const options = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'bar'
}
]
};
</script>
<template>
<div class="Box">
<div class="NavBox">
<span>近7天设备激活量</span>
<div class="actions">
<div>
<div class="title">本周新增</div>
<div class="num">3005</div>
</div>
<div>
<div class="title">日均激活</div>
<div class="num day">573</div>
</div>
</div>
</div>
<div class="ChartBox">
<Echarts :options="options"></Echarts>
</div>
</div>
</template>
<style lang="scss" scoped>
.Box {
padding: 25px;
height: 372px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
.NavBox {
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
span {
height: 22px;
line-height: 22px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 16px;
}
.actions {
display: flex;
align-items: center;
gap: 25px;
.title {
height: 16px;
line-height: 16px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.num {
width: 49px;
height: 24px;
line-height: 24px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 18px;
&.day {
color: rgba(37, 99, 235, 1);
}
}
}
}
.ChartBox {
height: 330px;
}
}
</style>

View File

@@ -0,0 +1,98 @@
<script lang="ts" name="AreaDevices" setup>
import {ref} from 'vue';
import {ElProgress} from 'element-plus';
const list = ref([
{
areaName: '华东区',
num: '3002台',
percent: 23,
},
{
areaName: '华北区',
num: '8002台',
percent: 50,
},
])
</script>
<template>
<div class="Box">
<div class="NavBox">
<div class="TypeNavBox">区域设备分布</div>
<div class="subTypeNavBox">共 12,860 台在线设备</div>
</div>
<div class="ChartBox">
<div v-for="(item,index) in list" :key="index" style="margin-bottom: 18px">
<div class="flex justify-between progressInfo">
<div>{{ item.areaName }}</div>
<div class="flex">
<div class="num">{{ item.num }}</div>
<div class="percent">{{ item.percent }}%</div>
</div>
</div>
<el-progress :percentage="item.percent" :show-text="false"/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.Box {
padding: 25px;
height: 372px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
.NavBox {
height: 36px;
margin-bottom: 20px;
.TypeNavBox {
height: 22px;
line-height: 22px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 16px;
}
.subTypeNavBox {
height: 16px;
line-height: 16px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
}
.ChartBox {
height: 310px;
overflow-y: auto;
.progressInfo {
margin-bottom: 8px;
.num {
height: 16px;
line-height: 16px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(156, 163, 175, 1);
font-size: 13px;
text-align: left;
}
.percent {
margin-left: 12px;
height: 16px;
line-height: 16px;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 13px;
}
}
}
}
</style>

View File

@@ -0,0 +1,154 @@
<script lang="ts" name="Cards" setup>
const cardList = ref([
{
title: '环境数据',
num: 12.3,
unit: '台',
rate: '32.2%',
upRate: true,
circleColor: "#2563eb",
color: "rgba(22, 163, 74, 1)",
rateColor: "rgba(22, 163, 74, 0.1)",
},
{
title: '设备在线率',
num: 0.08,
unit: '台',
rate: '98.6%',
upRate: false,
circleColor: "#6366f1",
color: "#ed3822",
rateColor: "rgb(217 80 64 / 0.27)",
},
{
title: '平均TVOC原始值',
num: 0.08,
unit: '台',
rate: '98.6%',
upRate: false,
circleColor: "#6366f1",
color: "#ed3822",
rateColor: "rgb(217 80 64 / 0.27)",
},
{
title: '今日设备激活',
num: 0.08,
unit: '台',
rate: '98.6%',
upRate: false,
circleColor: "#6366f1",
color: "#ed3822",
rateColor: "rgb(217 80 64 / 0.27)",
},
])
</script>
<template>
<div class="CardBox">
<div v-for="(item,index) in cardList" :key="index" class="card">
<div class="cardNav">
<div class="cardTitle">
<i :style="{backgroundColor:item.circleColor}" class="circle"></i>
<span>{{ item.title }}</span>
</div>
<div :style="{backgroundColor:item.rateColor}" class="cardRate">
<span v-if="item.upRate" :style="{color:item.color}" class="up">+{{ item.rate }}</span>
<span v-else :style="{color:item.color}" class="down">-{{ item.rate }}</span>
</div>
</div>
<div class="cardContent">
<div class="cardNum">{{ item.num }}</div>
<span>{{ item.unit }}</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.CardBox {
display: flex;
align-items: center;
gap: 20px;
.card {
flex: 389px;
max-width: 389px;
height: 120px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
padding: 21px;
.cardNav {
display: flex;
align-items: center;
justify-content: space-between;
.cardTitle {
display: flex;
align-items: center;
width: 100px;
height: 16px;
line-height: 16px;
background-color: rgba(0, 0, 0, 0);
color: rgba(107, 114, 128, 1);
font-size: 13px;
.circle {
display: block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(0, 0, 0);
margin-right: 4px;
}
}
.cardRate {
width: 52px;
height: 24px;
border-radius: 12px;
background-color: rgba(232, 247, 238, 1);
line-height: 24px;
color: rgba(22, 163, 74, 1);
font-size: 12px;
text-align: center;
.up {
color: rgba(22, 163, 74, 1);
}
.down {
color: rgba(22, 163, 74, 1);
}
}
}
.cardContent {
display: flex;
align-items: end;
margin-top: 20px;
.cardNum {
height: 36px;
line-height: 36px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 30px;
}
span {
width: 15px;
height: 17px;
line-height: 17px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(107, 114, 128, 1);
font-size: 14px;
}
}
}
}
</style>

View File

@@ -0,0 +1,113 @@
<script lang="ts" name="Tvoc" setup>
import Echarts from '@/components/Echarts/index.vue';
const options = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line'
}
]
};
const list = [
{
label: '24小时',
value: 24,
},
{
label: '7天',
value: 7,
},
{
label: '30天',
value: 30,
},
// +30 300, 99/1000 69, 59/20g/500
]
const activeDay = ref(24);
function changeActive(val: number) {
activeDay.value = val;
}
</script>
<template>
<div class="TvocBox">
<div class="TvocNavBox">
<span>平均TVOC原始值</span>
<div class="actions">
<div v-for="(item,index) in list" :key="index" :class="{
active: activeDay === item.value,
}" class="actionItem" @click="changeActive(item.value)">{{ item.label }}
</div>
</div>
</div>
<div class="TvocChartBox">
<Echarts :options="options"></Echarts>
</div>
</div>
</template>
<style lang="scss" scoped>
.TvocBox {
padding: 25px;
height: 372px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
.TvocNavBox {
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
span {
height: 22px;
line-height: 22px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 16px;
}
.actions {
width: 175px;
height: 36px;
border-radius: 8px 8px 8px 8px;
background-color: rgba(243, 245, 248, 1);
display: flex;
align-items: center;
justify-content: space-evenly;
padding: 4px;
.actionItem {
max-width: 64px;
height: 28px;
padding: 7px 12px;
color: rgba(107, 114, 128, 1);
font-size: 12px;
&.active {
color: rgba(37, 99, 235, 1);
border-radius: 6px 6px 6px 6px;
background-color: rgba(255, 255, 255, 1);
}
}
}
}
.TvocChartBox {
height: 330px;
}
}
</style>

View File

@@ -0,0 +1,108 @@
<script lang="ts" name="Type" setup>
import Echarts from '@/components/Echarts/index.vue';
const data = genData(7);
const options = ref({
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: data.legendData
},
series: [
{
name: '姓名',
type: 'pie',
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
center: ['38%', '50%'],
radius: ['40%', '70%'],
data: data.seriesData,
label: {
show: false,
position: 'center'
}
}
]
});
function genData(count: number) {
// prettier-ignore
const nameList = [
'赵', '钱', '孙', '李', '周', '吴', '郑', '王', '冯', '陈', '褚', '卫', '蒋', '沈', '韩', '杨', '朱', '秦', '尤', '许', '何', '吕', '施', '张', '孔', '曹', '严', '华', '金', '魏', '陶', '姜', '戚', '谢', '邹', '喻', '柏', '水', '窦', '章', '云', '苏', '潘', '葛', '奚', '范', '彭', '郎', '鲁', '韦', '昌', '马', '苗', '凤', '花', '方', '俞', '任', '袁', '柳', '酆', '鲍', '史', '唐', '费', '廉', '岑', '薛', '雷', '贺', '倪', '汤', '滕', '殷', '罗', '毕', '郝', '邬', '安', '常', '乐', '于', '时', '傅', '皮', '卞', '齐', '康', '伍', '余', '元', '卜', '顾', '孟', '平', '黄', '和', '穆', '萧', '尹', '姚', '邵', '湛', '汪', '祁', '毛', '禹', '狄', '米', '贝', '明', '臧', '计', '伏', '成', '戴', '谈', '宋', '茅', '庞', '熊', '纪', '舒', '屈', '项', '祝', '董', '梁', '杜', '阮', '蓝', '闵', '席', '季', '麻', '强', '贾', '路', '娄', '危'
];
const legendData = [];
const seriesData = [];
for (var i = 0; i < count; i++) {
var name =
Math.random() > 0.65
? makeWord(4, 1) + '·' + makeWord(3, 0)
: makeWord(2, 1);
legendData.push(name);
seriesData.push({
name: name,
value: Math.round(Math.random() * 100000)
});
}
return {
legendData: legendData,
seriesData: seriesData
};
function makeWord(max: number, min: number) {
const nameLen = Math.ceil(Math.random() * max + min);
const name = [];
for (var i = 0; i < nameLen; i++) {
name.push(nameList[Math.round(Math.random() * nameList.length - 1)]);
}
return name.join('');
}
}
</script>
<template>
<div class="TypeBox">
<div class="TypeNavBox">设备类型分布</div>
<div class="subTypeNavBox">共 12,860 台在线设备</div>
<div class="TypeChartBox">
<Echarts :options="options"></Echarts>
</div>
</div>
</template>
<style lang="scss" scoped>
.TypeBox {
padding: 25px;
height: 372px;
border-radius: 16px 16px 16px 16px;
border: 1px solid rgba(238, 240, 243, 1);
.TypeNavBox {
height: 22px;
line-height: 22px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(17, 24, 39, 1);
font-size: 16px;
}
.subTypeNavBox {
height: 16px;
line-height: 16px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(156, 163, 175, 1);
font-size: 12px;
}
.TypeChartBox {
height: 330px;
}
}
</style>

83
src/views/index/index.vue Normal file
View File

@@ -0,0 +1,83 @@
<template>
<div class="home">
<div class=" updateTimeBox">
<div class="updateTime">数据更新于 {{ updateTime }}</div>
<div class="tag">
<div>环境数据为原始读数</div>
</div>
</div>
<!-- 卡片-->
<Cards></Cards>
<el-row :gutter="20">
<el-col :sm="14" :xs="24">
<Tvoc/>
</el-col>
<el-col :sm="10" :xs="24">
<Type/>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :sm="14" :xs="24">
<ActiveDevices></ActiveDevices>
</el-col>
<el-col :sm="10" :xs="24">
<AreaDevices></AreaDevices>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" name="Index" setup>
import Cards from './compontents/card.vue';
import Tvoc from './compontents/tvoc.vue';
import Type from './compontents/type.vue';
import AreaDevices from './compontents/areaDevices.vue';
import ActiveDevices from './compontents/activeDevices.vue';
const updateTime = ref('12:50');
</script>
<style lang="scss" scoped>
.home {
display: flex;
flex-direction: column;
gap: 20px;
.updateTimeBox {
display: flex;
align-items: center;
.updateTime {
height: 16px;
line-height: 16px;
border-radius: 0;
background-color: rgba(0, 0, 0, 0);
color: rgba(107, 114, 128, 1);
font-size: 13px;
}
.tag {
width: 116px;
height: 20px;
line-height: 20px;
border-radius: 10px;
background-color: rgba(240, 253, 250, 1);
margin-left: 4px;
div {
text-align: center;
color: rgba(15, 118, 110, 1);
font-size: 11px;
}
}
}
.echartsBox {
display: flex;
gap: 20px;
}
}
</style>

View File

@@ -110,7 +110,7 @@
</el-input>
</div>
</el-form-item>
<!-- <el-form-item v-if="captchaEnabled" prop="code" class="captcha-row">
<el-form-item v-if="captchaEnabled" class="captcha-row" prop="code">
<el-input
v-model="loginForm.code"
size="large"
@@ -127,7 +127,7 @@
<div class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
</el-form-item> -->
</el-form-item>
<div class="form-meta">
<el-checkbox v-model="loginForm.rememberMe">{{
$t("login.rememberPassword")
@@ -550,6 +550,7 @@ onMounted(() => {
background-color: rgba(247, 248, 250, 1);
border: 1px solid rgba(238, 240, 243, 1);
.actions {
cursor: pointer;
color: rgba(37, 99, 235, 1);
}
}

View File

@@ -1,5 +1,6 @@
<template>
<div class="p-2 app-container system-client-page">
<Pagecrumb></Pagecrumb>
<div class="search-wrap">
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
<template #header>
@@ -26,7 +27,7 @@
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="状态" clearable>
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value"/>
</el-select>
</el-form-item>
<el-form-item>
@@ -36,7 +37,6 @@
</el-form>
</el-card>
</div>
<el-card shadow="hover" class="table-panel">
<template #header>
<div class="toolbar-shell">
@@ -82,19 +82,19 @@
class="data-table"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column v-if="false" label="id" align="center" prop="id" />
<el-table-column label="客户端id" align="center" prop="clientId" />
<el-table-column label="客户端key" align="center" prop="clientKey" />
<el-table-column label="客户端秘钥" align="center" prop="clientSecret" />
<el-table-column align="center" type="selection" width="55"/>
<el-table-column v-if="false" align="center" label="id" prop="id"/>
<el-table-column align="center" label="客户端id" prop="clientId"/>
<el-table-column align="center" label="客户端key" prop="clientKey"/>
<el-table-column align="center" label="客户端秘钥" prop="clientSecret"/>
<el-table-column label="授权类型" align="center">
<template #default="scope">
<dict-tag class="grant-type-tag" :options="sys_grant_type" :value="scope.row.grantTypeList" />
<dict-tag :options="sys_grant_type" :value="scope.row.grantTypeList" class="grant-type-tag"/>
</template>
</el-table-column>
<el-table-column label="设备类型" align="center">
<template #default="scope">
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
<dict-tag :options="sys_device_type" :value="scope.row.deviceType"/>
</template>
</el-table-column>
<el-table-column label="白名单路径" align="center">
@@ -132,8 +132,8 @@
</div>
</template>
</el-table-column>
<el-table-column label="Token活跃超时时间" align="center" prop="activeTimeout" />
<el-table-column label="Token固定超时时间" align="center" prop="timeout" />
<el-table-column align="center" label="Token活跃超时时间" prop="activeTimeout"/>
<el-table-column align="center" label="Token固定超时时间" prop="timeout"/>
<el-table-column key="status" label="状态" align="center">
<template #default="scope">
<el-switch
@@ -180,10 +180,10 @@
<el-dialog v-model="dialog.visible" :title="dialog.title" width="760px" append-to-body>
<el-form ref="clientFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="客户端key" prop="clientKey">
<el-input v-model="form.clientKey" :disabled="form.id != null" placeholder="请输入客户端key" />
<el-input v-model="form.clientKey" :disabled="form.id != null" placeholder="请输入客户端key"/>
</el-form-item>
<el-form-item label="客户端秘钥" prop="clientSecret">
<el-input v-model="form.clientSecret" :disabled="form.id != null" placeholder="请输入客户端秘钥" />
<el-input v-model="form.clientSecret" :disabled="form.id != null" placeholder="请输入客户端秘钥"/>
</el-form-item>
<el-form-item label="授权类型" prop="grantTypeList">
<el-select v-model="form.grantTypeList" multiple placeholder="请输入授权类型">
@@ -214,7 +214,7 @@
允许访问路径
</span>
</template>
<el-input v-model="form.accessPath" type="textarea" :rows="4" placeholder="示例:/app/**" />
<el-input v-model="form.accessPath" :rows="4" placeholder="示例:/app/**" type="textarea"/>
</el-form-item>
<el-form-item prop="ipWhitelist" label-width="auto">
<template #label>
@@ -244,7 +244,7 @@
Token活跃超时时间
</span>
</template>
<el-input v-model="form.activeTimeout" placeholder="请输入Token活跃超时时间" />
<el-input v-model="form.activeTimeout" placeholder="请输入Token活跃超时时间"/>
</el-form-item>
<el-form-item prop="timeout" label-width="auto">
<template #label>
@@ -255,7 +255,7 @@
Token固定超时时间
</span>
</template>
<el-input v-model="form.timeout" placeholder="请输入Token固定超时时间" />
<el-input v-model="form.timeout" placeholder="请输入Token固定超时时间"/>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
@@ -286,6 +286,8 @@ import { useTableSelection } from '@/hooks/table/useTableSelection';
import modal from '@/plugins/modal';
import { useDict } from '@/utils/dict';
import { download as requestDownload } from '@/utils/request';
import Pagecrumb from '@/components/Pagecrumb/index.vue';
const { sys_normal_disable } = toRefs<any>(useDict('sys_normal_disable'));
const { sys_grant_type } = toRefs<any>(useDict('sys_grant_type'));

View File

@@ -30,7 +30,7 @@ export default defineConfig(({ mode, command }) => {
open: true,
proxy: {
[env.VITE_APP_BASE_API]: {
target: 'http://localhost:8080',
target: 'http://localhost:5660',
changeOrigin: true,
ws: true,
rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')