项目初始化

This commit is contained in:
2026-03-26 18:48:48 +08:00
parent 239dfcb2e7
commit fa16e6d546
2 changed files with 358 additions and 1 deletions

View File

@@ -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>

View File

@@ -16,7 +16,9 @@
</div>
<div class="analysis-right">
<div class="analysis-right-top">
<section class="analysis-panel">右上-1</section>
<section class="analysis-panel">
<QuickLogin />
</section>
<section class="analysis-panel">右上-2</section>
</div>
<section class="analysis-panel">右下</section>
@@ -31,6 +33,8 @@
import HostInfo from './components/HostInfo.vue';
import TodoInfo from './components/TodoInfo.vue';
import NoticeInfo from './components/NoticeInfo.vue';
import QuickLogin from './components/QuickLogin.vue';
</script>
<style lang="less">
@dark-bg: #141414;