feat: 重构登录页与首页,替换项目品牌标识

1. 更新生产和开发环境的站点标题与logo标题为haiwei-wcs相关内容
2. 重写登录页面布局,新增品牌介绍面板,优化表单容器样式
3. 重构首页为WCS专属仪表盘,替换原有RuoYi框架介绍内容,新增数据统计卡片、图表和快捷入口
4. 调整页脚版权信息,动态显示当前年份和品牌标题
main
zch 4 days ago
parent 7530579692
commit 891410f9e1

@ -1,7 +1,6 @@
# 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
VITE_APP_TITLE = haiwei-wcs多租户管理系统
VITE_APP_LOGO_TITLE = haiwei-wcs
# 开发环境配置
VITE_APP_ENV = 'development'

@ -1,6 +1,6 @@
# 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus多租户管理系统
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus
VITE_APP_TITLE = haiwei-wcs租户管理系统
VITE_APP_LOGO_TITLE = haiwei-wcs
# 生产环境配置
VITE_APP_ENV = 'production'

@ -1,164 +1,485 @@
<template>
<div class="app-container home">
<el-row :gutter="20">
<el-col :sm="24" :lg="12" style="padding-left: 20px">
<h2>RuoYi-Vue-Plus多租户管理系统</h2>
<p>
RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
<br />
* 前端开发框架 Vue3TSElement Plus<br />
* 后端开发框架 Spring Boot<br />
* 容器框架 Undertow 基于 Netty 的高性能容器<br />
* 权限认证框架 Sa-Token 支持多终端认证系统<br />
* 关系数据库 MySQL 适配 8.X 最低 5.7<br />
* 缓存数据库 Redis 适配 6.X 最低 4.X<br />
* 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率<br />
* 数据库框架 p6spy 更强劲的 SQL 分析<br />
* 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构<br />
* 序列化框架 Jackson 统一使用 jackson 高效可靠<br />
* Redis客户端 Redisson 性能强劲API丰富<br />
* 分布式限流 Redisson 全局请求IP集群ID 多种限流<br />
* 分布式锁 Lock4j 注解锁工具锁 多种多样<br />
* 分布式幂等 Lock4j 基于分布式锁实现<br />
* 分布式链路追踪 SkyWalking 支持链路追踪网格分析度量聚合可视化<br />
* 分布式任务调度 SnailJob 高性能 高可靠 易扩展<br />
* 文件存储 Minio 本地存储<br />
* 文件存储 七牛阿里腾讯 云存储<br />
* 监控框架 SpringBoot-Admin 全方位服务监控<br />
* 校验框架 Validation 增强接口安全性 严谨性<br />
* Excel框架 FastExcel(原Alibaba EasyExcel) 性能优异 扩展性强<br />
* 文档框架 SpringDocjavadoc 无注解零入侵基于java注释<br />
* 工具类框架 HutoolLombok 减少代码冗余 增加安全性<br />
* 代码生成器 适配MPSpringDoc规范化代码 一键生成前后端代码<br />
* 部署方式 Docker 容器编排 一键部署业务集群<br />
* 国际化 SpringMessage Spring标准国际化方案<br />
</p>
<p><b>当前版本:</b> <span>v5.6.1</span></p>
<p>
<el-tag type="danger">&yen;免费开源</el-tag>
</p>
<p>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">访</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Vue-Plus')">访GitHub</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')"
>更新日志</el-button
>
</p>
</el-col>
<el-col :sm="24" :lg="12" style="padding-left: 20px">
<h2>haiwei-Cloud-Plus多租户微服务管理系统</h2>
<p>
haiwei-Cloud-Plus 微服务通用权限管理系统 重写 haiwei-Cloud 全方位升级(不兼容原框架)
<br />
* 前端开发框架 Vue3TSElement UI<br />
* 后端开发框架 Spring Boot<br />
* 微服务开发框架 Spring CloudSpring Cloud Alibaba<br />
* 容器框架 Undertow 基于 XNIO 的高性能容器<br />
* 权限认证框架 Sa-TokenJwt 支持多终端认证系统<br />
* 关系数据库 MySQL 适配 8.X 最低 5.7<br />
* 关系数据库 Oracle 适配 11g 12c<br />
* 关系数据库 PostgreSQL 适配 13 14<br />
* 关系数据库 SQLServer 适配 2017 2019<br />
* 缓存数据库 Redis 适配 6.X 最低 5.X<br />
* 分布式注册中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
* 分布式配置中心 Alibaba Nacos 采用2.X 基于GRPC通信高性能<br />
* 服务网关 Spring Cloud Gateway 响应式高性能网关<br />
* 负载均衡 Spring Cloud Loadbalancer 负载均衡处理<br />
* RPC远程调用 Apache Dubbo 原生态使用体验高性能<br />
* 分布式限流熔断 Alibaba Sentinel 无侵入高扩展<br />
* 分布式事务 Alibaba Seata 无侵入高扩展 支持 四种模式<br />
* 分布式消息队列 Apache Kafka 高性能高速度<br />
* 分布式消息队列 Apache RocketMQ 高可用功能多样<br />
* 分布式消息队列 RabbitMQ 支持各种扩展插件功能多样性<br />
* 分布式搜索引擎 ElasticSearch 业界知名<br />
* 分布式链路追踪 Apache SkyWalking 链路追踪网格分析度量聚合可视化<br />
* 分布式日志中心 ELK 业界成熟解决方案<br />
* 分布式监控 PrometheusGrafana 全方位性能监控<br />
* 其余与 Vue 版本一致<br />
</p>
<p><b>当前版本:</b> <span>v2.6.1</span></p>
<p>
<el-tag type="danger">&yen;免费开源</el-tag>
</p>
<p>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://gitee.com/dromara/RuoYi-Cloud-Plus')">访</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://github.com/dromara/RuoYi-Cloud-Plus')">访GitHub</el-button>
<el-button type="primary" icon="Cloudy" plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog')"
>更新日志</el-button
>
</p>
<!-- 欢迎栏 -->
<el-row :gutter="20" class="mb-4">
<el-col :span="24">
<el-card shadow="never">
<div class="welcome-header">
<div>
<h2 class="welcome-title">{{ $t('dashboard.welcome') }}</h2>
<p class="welcome-desc">{{ $t('dashboard.description') }}</p>
</div>
<div class="welcome-meta">
<el-tag type="primary">{{ currentDate }}</el-tag>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 核心指标 -->
<el-row :gutter="20" class="mb-4">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="4" v-for="item in statCards" :key="item.key">
<el-card shadow="hover" class="stat-card" @click="goTarget(item.path)">
<div class="stat-icon" :style="{ background: item.color }">
<svg-icon :icon-class="item.icon" />
</div>
<div class="stat-info">
<div class="stat-value">{{ item.value }}</div>
<div class="stat-label">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 状态分布图表 -->
<el-row :gutter="20" class="mb-4">
<el-col :xs="24" :md="12" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.taskStatus') }}</span>
</template>
<div ref="taskStatusChartRef" class="chart-box"></div>
</el-card>
</el-col>
<el-col :xs="24" :md="12" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.deviceStatus') }}</span>
</template>
<div ref="deviceStatusChartRef" class="chart-box"></div>
</el-card>
</el-col>
<el-col :xs="24" :md="12" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.locationStatus') }}</span>
</template>
<div ref="locationStatusChartRef" class="chart-box"></div>
</el-card>
</el-col>
</el-row>
<!-- 最近任务与快捷入口 -->
<el-row :gutter="20">
<el-col :xs="24" :lg="16" class="mb-4">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>{{ $t('dashboard.recentTasks') }}</span>
<el-button link type="primary" @click="goTarget('/base/taskQueue')">{{ $t('dashboard.more') }}</el-button>
</div>
</template>
<el-table v-loading="loading" :data="recentTasks" stripe>
<el-table-column prop="taskCode" :label="$t('dashboard.taskCode')" min-width="120" />
<el-table-column prop="materialName" :label="$t('dashboard.materialName')" min-width="120" />
<el-table-column prop="startPoint" :label="$t('dashboard.startPoint')" min-width="100" />
<el-table-column prop="endPoint" :label="$t('dashboard.endPoint')" min-width="100" />
<el-table-column prop="taskStatus" :label="$t('dashboard.taskStatus')" width="100">
<template #default="scope">
<dict-tag :options="wcs_task_status" :value="scope.row.taskStatus" />
</template>
</el-table-column>
<el-table-column prop="createTime" :label="$t('dashboard.createTime')" width="170">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, CREATED_TIME_DISPLAY_FORMAT) }}</span>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
<el-col :xs="24" :lg="8">
<el-card shadow="never">
<template #header>
<span>{{ $t('dashboard.quickLinks') }}</span>
</template>
<div class="quick-link-list">
<div v-for="link in quickLinks" :key="link.path" class="quick-link" @click="goTarget(link.path)">
<svg-icon :icon-class="link.icon" class="quick-link-icon" />
<div class="quick-link-info">
<div class="quick-link-title">{{ link.title }}</div>
<div class="quick-link-desc">{{ link.desc }}</div>
</div>
<svg-icon icon-class="caret-forward" class="quick-link-arrow" />
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-divider />
</div>
</template>
<script setup name="Index" lang="ts">
const goTarget = (url: string) => {
window.open(url, '__blank');
import * as echarts from 'echarts';
import { parseTime } from '@/utils/ruoyi';
import { countTaskQueue, listTaskQueue } from '@/api/wcs/taskQueue';
import { countTaskDetail } from '@/api/wcs/taskDetail';
import { countDeviceInfo } from '@/api/wcs/deviceInfo';
import { countDeviceParam } from '@/api/wcs/deviceParam';
import { countLocationInfo } from '@/api/wcs/locationInfo';
import { countMaterialInfo } from '@/api/wcs/materialInfo';
import { countStoreInfo } from '@/api/wcs/storeInfo';
import { countPathInfo } from '@/api/wcs/pathInfo';
import { countDeviceHost } from '@/api/wcs/deviceHost';
import { TaskQueueVO } from '@/api/wcs/taskQueue/types';
import { CREATED_TIME_DISPLAY_FORMAT } from './wcs/utils/createTimeRange';
import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { t } = useI18n();
const router = useRouter();
const { wcs_task_status } = toRefs<any>(proxy?.useDict('wcs_task_status'));
const loading = ref(false);
const currentDate = ref('');
const statCards = ref([
{ key: 'taskQueue', label: t('dashboard.taskQueue'), value: 0, icon: 'list', color: '#3b82f6', path: '/base/taskQueue' },
{ key: 'taskDetail', label: t('dashboard.taskDetail'), value: 0, icon: 'component', color: '#6366f1', path: '/base/taskDetail' },
{ key: 'deviceHost', label: t('dashboard.deviceHost'), value: 0, icon: 'server', color: '#64748b', path: '/base/deviceHost' },
{ key: 'deviceInfo', label: t('dashboard.deviceInfo'), value: 0, icon: 'chart', color: '#10b981', path: '/base/deviceInfo' },
{ key: 'deviceParam', label: t('dashboard.deviceParam'), value: 0, icon: 'edit', color: '#0ea5e9', path: '/base/deviceParam' },
{ key: 'locationInfo', label: t('dashboard.locationInfo'), value: 0, icon: 'dict', color: '#f59e0b', path: '/base/locationInfo' },
{ key: 'materialInfo', label: t('dashboard.materialInfo'), value: 0, icon: 'excel', color: '#8b5cf6', path: '/base/materialInfo' },
{ key: 'storeInfo', label: t('dashboard.storeInfo'), value: 0, icon: 'tree', color: '#ec4899', path: '/base/storeInfo' },
{ key: 'pathInfo', label: t('dashboard.pathInfo'), value: 0, icon: 'build', color: '#14b8a6', path: '/base/pathInfo' }
]);
const quickLinks = ref([
{ title: t('dashboard.taskQueue'), desc: t('dashboard.taskQueueDesc'), icon: 'list', path: '/base/taskQueue' },
{ title: t('dashboard.taskDetail'), desc: t('dashboard.taskDetailDesc'), icon: 'component', path: '/base/taskDetail' },
{ title: t('dashboard.deviceHost'), desc: t('dashboard.deviceHostDesc'), icon: 'server', path: '/base/deviceHost' },
{ title: t('dashboard.deviceInfo'), desc: t('dashboard.deviceInfoDesc'), icon: 'chart', path: '/base/deviceInfo' },
{ title: t('dashboard.deviceParam'), desc: t('dashboard.deviceParamDesc'), icon: 'edit', path: '/base/deviceParam' },
{ title: t('dashboard.locationInfo'), desc: t('dashboard.locationInfoDesc'), icon: 'dict', path: '/base/locationInfo' },
{ title: t('dashboard.materialInfo'), desc: t('dashboard.materialInfoDesc'), icon: 'excel', path: '/base/materialInfo' },
{ title: t('dashboard.storeInfo'), desc: t('dashboard.storeInfoDesc'), icon: 'tree', path: '/base/storeInfo' },
{ title: t('dashboard.pathInfo'), desc: t('dashboard.pathInfoDesc'), icon: 'build', path: '/base/pathInfo' }
]);
const recentTasks = ref<TaskQueueVO[]>([]);
const taskStatusChartRef = ref<HTMLDivElement>();
const deviceStatusChartRef = ref<HTMLDivElement>();
const locationStatusChartRef = ref<HTMLDivElement>();
let taskStatusChart: echarts.ECharts | null = null;
let deviceStatusChart: echarts.ECharts | null = null;
let locationStatusChart: echarts.ECharts | null = null;
const goTarget = (path: string) => {
if (path.startsWith('http')) {
window.open(path, '__blank');
} else {
router.push(path);
}
};
const formatDate = (date: Date) => {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${weekdays[date.getDay()]}`;
};
const getCount = (res: any) => (res && typeof res.data === 'number' ? res.data : 0);
const loadCounts = async () => {
const [taskQueueRes, taskDetailRes, hostRes, deviceRes, deviceParamRes, locationRes, materialRes, storeRes, pathRes] = await Promise.all([
countTaskQueue(),
countTaskDetail(),
countDeviceHost(),
countDeviceInfo(),
countDeviceParam(),
countLocationInfo(),
countMaterialInfo(),
countStoreInfo(),
countPathInfo()
]);
statCards.value[0].value = getCount(taskQueueRes);
statCards.value[1].value = getCount(taskDetailRes);
statCards.value[2].value = getCount(hostRes);
statCards.value[3].value = getCount(deviceRes);
statCards.value[4].value = getCount(deviceParamRes);
statCards.value[5].value = getCount(locationRes);
statCards.value[6].value = getCount(materialRes);
statCards.value[7].value = getCount(storeRes);
statCards.value[8].value = getCount(pathRes);
};
const loadTaskStatusChart = async () => {
const statusOptions = wcs_task_status.value || [];
if (!statusOptions.length) {
taskStatusChart?.setOption({
title: { text: t('dashboard.noData'), left: 'center', top: 'center', textStyle: { fontSize: 14, color: '#999' } },
series: []
});
return;
}
const data = await Promise.all(
statusOptions.map(async (dict: any) => {
const res = await countTaskQueue({ taskStatus: parseInt(dict.value) });
return { name: dict.label, value: getCount(res) };
})
);
taskStatusChart?.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
series: [
{
type: 'pie',
radius: '65%',
data,
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}
]
});
};
const loadDeviceStatusChart = async () => {
const statuses = [
{ label: t('dashboard.deviceNormal'), value: 0 },
{ label: t('dashboard.deviceBusy'), value: 1 },
{ label: t('dashboard.deviceAbnormal'), value: 2 }
];
const data = await Promise.all(
statuses.map(async (status) => {
const res = await countDeviceInfo({ deviceStatus: status.value });
return { name: status.label, value: getCount(res) };
})
);
deviceStatusChart?.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
color: ['#10b981', '#f59e0b', '#ef4444'],
series: [
{
type: 'pie',
radius: ['40%', '65%'],
data,
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}
]
});
};
const loadLocationStatusChart = async () => {
const statuses = [
{ label: t('dashboard.locationUnused'), value: 0 },
{ label: t('dashboard.locationUsed'), value: 1 },
{ label: t('dashboard.locationLocked'), value: 2 },
{ label: t('dashboard.locationAbnormal'), value: 3 }
];
const data = await Promise.all(
statuses.map(async (status) => {
const res = await countLocationInfo({ locationStatus: status.value });
return { name: status.label, value: getCount(res) };
})
);
locationStatusChart?.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: 0 },
color: ['#94a3b8', '#3b82f6', '#f59e0b', '#ef4444'],
series: [
{
type: 'pie',
radius: ['40%', '65%'],
data,
emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}
]
});
};
const loadRecentTasks = async () => {
loading.value = true;
try {
const res = await listTaskQueue({ pageNum: 1, pageSize: 5, params: {} });
recentTasks.value = res.rows || [];
} finally {
loading.value = false;
}
};
const initCharts = () => {
if (taskStatusChartRef.value) taskStatusChart = echarts.init(taskStatusChartRef.value);
if (deviceStatusChartRef.value) deviceStatusChart = echarts.init(deviceStatusChartRef.value);
if (locationStatusChartRef.value) locationStatusChart = echarts.init(locationStatusChartRef.value);
};
const loadCharts = async () => {
await Promise.all([loadTaskStatusChart(), loadDeviceStatusChart(), loadLocationStatusChart()]);
};
const handleResize = () => {
taskStatusChart?.resize();
deviceStatusChart?.resize();
locationStatusChart?.resize();
};
onMounted(() => {
currentDate.value = formatDate(new Date());
initCharts();
Promise.all([loadCounts(), loadRecentTasks(), loadCharts()]);
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
taskStatusChart?.dispose();
deviceStatusChart?.dispose();
locationStatusChart?.dispose();
});
</script>
<style lang="scss" scoped>
.home {
blockquote {
padding: 10px 20px;
margin: 0 0 20px;
font-size: 17.5px;
border-left: 5px solid #eee;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eee;
}
.col-item {
margin-bottom: 20px;
}
ul {
padding: 0;
margin: 0;
}
font-family: 'open sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 13px;
color: #676a6c;
color: var(--el-text-color-regular);
overflow-x: hidden;
ul {
list-style-type: none;
.mb-4 {
margin-bottom: 16px;
}
h4 {
margin-top: 0px;
}
.welcome-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
h2 {
margin-top: 10px;
font-size: 26px;
font-weight: 100;
}
.welcome-title {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: var(--el-text-color-primary);
}
p {
margin-top: 10px;
.welcome-desc {
margin: 0;
font-size: 14px;
color: var(--el-text-color-secondary);
}
b {
font-weight: 700;
.welcome-meta {
font-size: 14px;
}
}
.update-log {
ol {
display: block;
list-style-type: decimal;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0;
margin-inline-end: 0;
padding-inline-start: 40px;
.stat-card {
cursor: pointer;
transition:
transform 0.2s,
box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: var(--app-shadow-md);
}
:deep(.el-card__body) {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
}
.stat-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
flex-shrink: 0;
.svg-icon {
width: 26px;
height: 26px;
}
}
.stat-info {
flex: 1;
min-width: 0;
.stat-value {
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
line-height: 1.2;
}
.stat-label {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
}
}
.chart-box {
width: 100%;
height: 280px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.quick-link-list {
display: flex;
flex-direction: column;
gap: 8px;
.quick-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: var(--app-radius-md);
cursor: pointer;
transition: background 0.2s;
&:hover {
background: var(--el-fill-color-light);
}
.quick-link-icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.quick-link-info {
flex: 1;
min-width: 0;
.quick-link-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.quick-link-desc {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-top: 2px;
}
}
.quick-link-arrow {
color: var(--el-text-color-secondary);
}
}
}
}

@ -1,79 +1,99 @@
<template>
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<div class="title-box">
<h3 class="title">{{ title }}</h3>
<lang-select />
<div class="login-brand-panel">
<div class="brand-mask"></div>
<div class="brand-content">
<div class="brand-logo">
<svg-icon icon-class="dashboard" class="logo-icon" />
<span class="logo-title">{{ logoTitle }}</span>
</div>
<h1 class="brand-title">{{ title }}</h1>
<p class="brand-subtitle">仓库控制系统WCS</p>
<ul class="brand-features">
<li>仓库 / 库位 / 物料主数据统一管理</li>
<li>输送线AGV提升机等设备统一调度</li>
<li>实时任务队列与路径执行可视化</li>
<li>多租户隔离安全可靠的权限体系</li>
</ul>
</div>
<el-form-item v-if="tenantEnabled" prop="tenantId">
<el-select v-model="loginForm.tenantId" filterable :placeholder="proxy.$t('login.selectPlaceholder')" style="width: 100%">
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
</el-select>
</el-form-item>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" :placeholder="proxy.$t('login.username')">
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
size="large"
auto-complete="off"
:placeholder="proxy.$t('login.password')"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item v-if="captchaEnabled" prop="code">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
:placeholder="proxy.$t('login.code')"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
<div class="login-form-wrapper">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<div class="title-box">
<h3 class="title">{{ title }}</h3>
<lang-select />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
<el-form-item style="float: right">
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">
<svg-icon icon-class="wechat" />
</el-button>
<el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')">
<svg-icon icon-class="maxkey" />
</el-button>
<el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')">
<svg-icon icon-class="topiam" />
</el-button>
<el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')">
<svg-icon icon-class="gitee" />
</el-button>
<el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')">
<svg-icon icon-class="github" />
</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
<span v-else>{{ proxy.$t('login.logging') }}</span>
</el-button>
<div v-if="register" style="float: right">
<router-link class="link-type" :to="'/register'">{{ proxy.$t('login.switchRegisterPage') }}</router-link>
</div>
</el-form-item>
</el-form>
<el-form-item v-if="tenantEnabled" prop="tenantId">
<el-select v-model="loginForm.tenantId" filterable :placeholder="proxy.$t('login.selectPlaceholder')" style="width: 100%">
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
</el-select>
</el-form-item>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" :placeholder="proxy.$t('login.username')">
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
size="large"
auto-complete="off"
:placeholder="proxy.$t('login.password')"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item v-if="captchaEnabled" prop="code">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
:placeholder="proxy.$t('login.code')"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" class="login-code-img" @click="getCode" />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
<el-form-item style="float: right">
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">
<svg-icon icon-class="wechat" />
</el-button>
<el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')">
<svg-icon icon-class="maxkey" />
</el-button>
<el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')">
<svg-icon icon-class="topiam" />
</el-button>
<el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')">
<svg-icon icon-class="gitee" />
</el-button>
<el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')">
<svg-icon icon-class="github" />
</el-button>
</el-form-item>
<el-form-item style="width: 100%">
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
<span v-else>{{ proxy.$t('login.logging') }}</span>
</el-button>
<div v-if="register" style="float: right">
<router-link class="link-type" :to="'/register'">{{ proxy.$t('login.switchRegisterPage') }}</router-link>
</div>
</el-form-item>
</el-form>
</div>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
<span>Copyright © {{ currentYear }} {{ logoTitle }} All Rights Reserved.</span>
</div>
</div>
</template>
@ -90,6 +110,8 @@ import { useI18n } from 'vue-i18n';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const title = import.meta.env.VITE_APP_TITLE;
const logoTitle = import.meta.env.VITE_APP_LOGO_TITLE;
const currentYear = new Date().getFullYear();
const userStore = useUserStore();
const router = useRouter();
const { t } = useI18n();
@ -235,12 +257,111 @@ onMounted(() => {
<style lang="scss" scoped>
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 100vh;
background-image: url('../assets/images/login-background.jpg');
background-size: cover;
background-position: center;
position: relative;
}
.login-brand-panel {
display: none;
flex: 1;
position: relative;
color: #ffffff;
padding: 60px;
align-items: center;
overflow: hidden;
.brand-mask {
position: absolute;
inset: 0;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.88), rgba(30, 58, 138, 0.78));
}
.brand-content {
position: relative;
z-index: 1;
max-width: 560px;
}
.brand-logo {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 40px;
.logo-icon {
width: 42px;
height: 42px;
color: #60a5fa;
}
.logo-title {
font-size: 22px;
font-weight: 700;
letter-spacing: 1px;
}
}
.brand-title {
font-size: 38px;
font-weight: 700;
margin: 0 0 12px 0;
line-height: 1.2;
}
.brand-subtitle {
font-size: 20px;
font-weight: 400;
margin: 0 0 40px 0;
color: rgba(255, 255, 255, 0.82);
letter-spacing: 2px;
}
.brand-features {
list-style: none;
padding: 0;
margin: 0;
font-size: 16px;
line-height: 2.2;
color: rgba(255, 255, 255, 0.78);
li {
position: relative;
padding-left: 24px;
&::before {
content: '';
position: absolute;
left: 0;
top: 14px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #60a5fa;
}
}
}
@media (min-width: 992px) {
display: flex;
}
}
.login-form-wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 24px;
z-index: 1;
@media (min-width: 992px) {
width: 520px;
min-width: 520px;
}
}
.title-box {
@ -266,7 +387,7 @@ onMounted(() => {
border-radius: var(--app-radius-lg);
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(255, 255, 255, 0.5);
width: min(420px, 90vw);
width: min(420px, 100%);
padding: 32px 30px 12px 30px;
z-index: 1;
box-shadow: var(--app-shadow-lg);
@ -340,14 +461,16 @@ onMounted(() => {
.el-login-footer {
height: 40px;
line-height: 40px;
position: fixed;
position: absolute;
bottom: 0;
width: 100%;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.75);
font-family: Arial, serif;
font-size: 12px;
letter-spacing: 1px;
z-index: 2;
}
.login-code-img {

Loading…
Cancel
Save