项目初始化
This commit is contained in:
@@ -0,0 +1,353 @@
|
|||||||
|
<template>
|
||||||
|
<div class="quick-login-card">
|
||||||
|
<div class="card-title">
|
||||||
|
<span>快捷登录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<el-button
|
||||||
|
class="quick-login-nav quick-login-nav--left"
|
||||||
|
circle
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:icon="ArrowLeftBold"
|
||||||
|
:disabled="!canScroll"
|
||||||
|
@click="scrollPrev"
|
||||||
|
/>
|
||||||
|
<div ref="viewportRef" class="quick-login-viewport" @mouseenter="pauseAutoScroll" @mouseleave="startAutoScroll">
|
||||||
|
<div
|
||||||
|
class="quick-login-track"
|
||||||
|
:style="{
|
||||||
|
transform: `translateX(-${currentOffset}px)`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="item in displayList"
|
||||||
|
:key="item.renderKey"
|
||||||
|
class="quick-login-item"
|
||||||
|
type="button"
|
||||||
|
@click="handleLogin(item)"
|
||||||
|
>
|
||||||
|
<div class="quick-login-item__image-wrap">
|
||||||
|
<img class="quick-login-item__image" :src="item.logo" :alt="item.name" />
|
||||||
|
</div>
|
||||||
|
<div class="quick-login-item__name">{{ item.name }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
class="quick-login-nav quick-login-nav--right"
|
||||||
|
circle
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:icon="ArrowRightBold"
|
||||||
|
:disabled="!canScroll"
|
||||||
|
@click="scrollNext"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
interface QuickLoginItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
logo: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderQuickLoginItem extends QuickLoginItem {
|
||||||
|
renderKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseList = ref<QuickLoginItem[]>([
|
||||||
|
{
|
||||||
|
id: 'oa',
|
||||||
|
name: '协同办公',
|
||||||
|
logo: 'https://dummyimage.com/96x96/2563eb/ffffff.png&text=OA',
|
||||||
|
url: '/oa',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'erp',
|
||||||
|
name: 'ERP系统',
|
||||||
|
logo: 'https://dummyimage.com/96x96/0891b2/ffffff.png&text=ERP',
|
||||||
|
url: '/erp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mes',
|
||||||
|
name: 'MES平台',
|
||||||
|
logo: 'https://dummyimage.com/96x96/7c3aed/ffffff.png&text=MES',
|
||||||
|
url: '/mes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'crm',
|
||||||
|
name: 'CRM系统',
|
||||||
|
logo: 'https://dummyimage.com/96x96/ea580c/ffffff.png&text=CRM',
|
||||||
|
url: '/crm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hr',
|
||||||
|
name: '人资门户',
|
||||||
|
logo: 'https://dummyimage.com/96x96/16a34a/ffffff.png&text=HR',
|
||||||
|
url: '/hr',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bi',
|
||||||
|
name: '数据驾驶舱',
|
||||||
|
logo: 'https://dummyimage.com/96x96/db2777/ffffff.png&text=BI',
|
||||||
|
url: '/bi',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const itemWidth = 104;
|
||||||
|
const gapWidth = 2;
|
||||||
|
const stepWidth = itemWidth + gapWidth;
|
||||||
|
const visibleCount = ref(1);
|
||||||
|
const currentIndex = ref(0);
|
||||||
|
const viewportRef = ref<HTMLElement>();
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
let autoScrollTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
const canScroll = computed(() => baseList.value.length > visibleCount.value);
|
||||||
|
|
||||||
|
const maxIndex = computed(() => {
|
||||||
|
const value = baseList.value.length - visibleCount.value;
|
||||||
|
return value > 0 ? value : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentOffset = computed(() => currentIndex.value * stepWidth);
|
||||||
|
|
||||||
|
const displayList = computed<RenderQuickLoginItem[]>(() => {
|
||||||
|
return baseList.value.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
renderKey: `${item.id}-${index}`,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateVisibleCount() {
|
||||||
|
const width = viewportRef.value?.clientWidth || 0;
|
||||||
|
if (!width) return;
|
||||||
|
const count = Math.max(1, Math.floor((width + gapWidth) / stepWidth));
|
||||||
|
visibleCount.value = count;
|
||||||
|
if (currentIndex.value > maxIndex.value) {
|
||||||
|
currentIndex.value = maxIndex.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollNext() {
|
||||||
|
if (!canScroll.value) return;
|
||||||
|
currentIndex.value = currentIndex.value >= maxIndex.value ? 0 : currentIndex.value + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollPrev() {
|
||||||
|
if (!canScroll.value) return;
|
||||||
|
currentIndex.value = currentIndex.value <= 0 ? maxIndex.value : currentIndex.value - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoScroll() {
|
||||||
|
if (!canScroll.value) return;
|
||||||
|
pauseAutoScroll();
|
||||||
|
autoScrollTimer = setInterval(() => {
|
||||||
|
scrollNext();
|
||||||
|
}, 2600);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseAutoScroll() {
|
||||||
|
if (autoScrollTimer) {
|
||||||
|
clearInterval(autoScrollTimer);
|
||||||
|
autoScrollTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogin(item: QuickLoginItem) {
|
||||||
|
ElMessage.success(`准备进入:${item.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
updateVisibleCount();
|
||||||
|
startAutoScroll();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (viewportRef.value) {
|
||||||
|
resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateVisibleCount();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(viewportRef.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
pauseAutoScroll();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.quick-login-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
color: rgb(51 65 85);
|
||||||
|
border-bottom: 1px solid rgb(226 232 240);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 2px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-nav {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24px;
|
||||||
|
height: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-viewport {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-track {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
transition: transform 0.45s ease;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-item {
|
||||||
|
flex: 0 0 104px;
|
||||||
|
width: 104px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
padding: 4px 4px 3px;
|
||||||
|
border: 1px solid rgb(226 232 240);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgb(248 250 252);
|
||||||
|
box-shadow: 0 8px 22px rgb(148 163 184 / 14%);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgb(147 197 253);
|
||||||
|
box-shadow: 0 12px 28px rgb(96 165 250 / 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 26px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding-top: 2px;
|
||||||
|
border-top: 1px solid rgb(226 232 240);
|
||||||
|
color: rgb(51 65 85);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] .quick-login-card {
|
||||||
|
.card-title {
|
||||||
|
color: rgb(203 213 225);
|
||||||
|
border-bottom-color: rgb(51 65 85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-item {
|
||||||
|
border-color: rgb(51 65 85);
|
||||||
|
background: rgb(20, 20, 20);
|
||||||
|
box-shadow: 0 10px 24px rgb(0 0 0 / 24%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgb(96 165 250);
|
||||||
|
box-shadow: 0 14px 30px rgb(37 99 235 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
border-top-color: rgb(51 65 85);
|
||||||
|
color: rgb(226 232 240);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.quick-login-card {
|
||||||
|
.card-content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-item {
|
||||||
|
flex-basis: 100px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-login-nav {
|
||||||
|
width: 22px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,7 +16,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="analysis-right">
|
<div class="analysis-right">
|
||||||
<div class="analysis-right-top">
|
<div class="analysis-right-top">
|
||||||
<section class="analysis-panel">右上-1</section>
|
<section class="analysis-panel">
|
||||||
|
<QuickLogin />
|
||||||
|
</section>
|
||||||
<section class="analysis-panel">右上-2</section>
|
<section class="analysis-panel">右上-2</section>
|
||||||
</div>
|
</div>
|
||||||
<section class="analysis-panel">右下</section>
|
<section class="analysis-panel">右下</section>
|
||||||
@@ -31,6 +33,8 @@
|
|||||||
import HostInfo from './components/HostInfo.vue';
|
import HostInfo from './components/HostInfo.vue';
|
||||||
import TodoInfo from './components/TodoInfo.vue';
|
import TodoInfo from './components/TodoInfo.vue';
|
||||||
import NoticeInfo from './components/NoticeInfo.vue';
|
import NoticeInfo from './components/NoticeInfo.vue';
|
||||||
|
import QuickLogin from './components/QuickLogin.vue';
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
@dark-bg: #141414;
|
@dark-bg: #141414;
|
||||||
|
|||||||
Reference in New Issue
Block a user