feat 首页

This commit is contained in:
Ironsp
2026-10-08 17:26:33 +08:00
parent a3b04485dd
commit aa440d6bb5
21 changed files with 138 additions and 926 deletions

View File

@@ -85,6 +85,11 @@ const coreBusiness = [
<style lang="less" scoped>
.app-footer {
// 设计稿:footer 顶 y=1942,总高 481px。各行绝对坐标换算为相对 footer 顶:
// 列标题 54 / 内容首行 115 / legal[0] 332 / legal[1] 371。
min-height: @footer-height;
display: flex;
flex-direction: column;
background: @footer-bg;
color: @white;
@@ -95,7 +100,7 @@ const coreBusiness = [
margin: 0 auto;
height: 250px;
box-sizing: border-box;
padding-top: 30px;
padding-top: 54px;
padding-left: clamp(
@spacing-base,
calc((100% - 1609px) / 2),
@@ -182,6 +187,8 @@ const coreBusiness = [
margin: 0 auto;
padding-bottom: 20px;
text-align: left;
// legal[0] 需落在相对 footer 顶 332px 处
margin-top: 62px;
}
&__legal {

View File

@@ -84,25 +84,16 @@ onMounted(() => void messageStore.fetchUnreadCount())
&__inner {
.flex(row, flex-start);
height: 100%;
// padding-left: clamp(@spacing-base, calc((100% - 1600px) / 2), 155px);
padding-left: 15%;
// padding-right: clamp(@spacing-base, calc((100% - 1680px) / 2), 120px);
padding-right: 15%;
// 设计稿 1920 基准:导航首项左 275px,操作区右边界 1800px(右侧留白 120px)。
// 视口窄于 1920 时按留白收缩,避免两端顶死。
padding-left: clamp(@spacing-base, calc((100% - 1370px) / 2), 275px);
padding-right: clamp(@spacing-base, calc((100% - 1680px) / 2), 120px);
}
&__nav {
.flex(row, center);
gap: @header-nav-gap;
height: 100%;
margin-left: 0px;
@media (max-width: 1600px) {
gap: 48px;
}
@media (max-width: 1280px) {
gap: 32px;
}
&-item {
position: relative;

View File

@@ -21,11 +21,7 @@ const showFooter = computed(() => !route.meta.noFooter)
<style lang="less" scoped>
.layout {
.flex(column, flex-start, stretch);
min-height: calc(100vh / @page-zoom);
@media (max-width: 1286px) {
min-height: calc(100vh / @page-zoom--sm);
}
min-height: 100vh;
&__main {
flex: 1;

View File

@@ -10,7 +10,8 @@
.container--home {
padding: 0 @home-gutter;
@media (max-width: 1919px) {
// 1:1 还原基准为 1920。仅当视口明显窄于 1920 时才收窄留白。
@media (max-width: 1600px) {
padding: 0 @spacing-lg;
}
}
@@ -18,13 +19,9 @@
.auth-page {
position: relative;
display: flex;
min-height: calc(100vh / @page-zoom);
min-height: 100vh;
background: @white;
@media (max-width: 1286px) {
min-height: calc(100vh / @page-zoom--sm);
}
&__brand {
.flex(column, flex-start, center);
flex: 0 0 @login-brand-width;

View File

@@ -1,10 +1,8 @@
@import './reset.less';
@import './common.less';
// 视觉基准:蓝湖设计稿 1920px,1:1 还原,不做整体缩放。
// 如需适配窄屏,请在各页面内部用媒体查询处理,不要在根元素缩放。
html {
zoom: @page-zoom;
@media (max-width: 1286px) {
zoom: @page-zoom--sm;
}
}
zoom: 1;
}

View File

@@ -56,8 +56,10 @@
@container-width: 1920px;
@page-zoom: 0.67;
@page-zoom--sm: 0.66;
// 视觉基准:蓝湖设计稿 1920px,1:1 还原,根元素不做缩放(html { zoom: 1 })。
// @page-zoom 仅作为「设计稿 px 与实际渲染 px」的换算说明保留,实际值为 1。
@page-zoom: 1;
@page-zoom--sm: 1;
@page-content: 1780px;
@page-gutter: 70px;

View File

@@ -241,15 +241,17 @@ function clearChips(): void {
>
{{ shortcut.label }}
</button>
<ElDatePicker
v-model="dateRange"
class="case-filter__date"
type="daterange"
range-separator="—"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
/>
<div class="case-filter__date-wrap">
<ElDatePicker
v-model="dateRange"
class="case-filter__date"
type="daterange"
range-separator="—"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
/>
</div>
</div>
</section>
@@ -433,16 +435,37 @@ function clearChips(): void {
}
}
&__date {
// ElDatePicker 有两层元素:外层 div.case-filter__date 与内部 .el-range-editor,
// 真正控制盒子尺寸的是内层。所以尺寸/背景/圆角都要用 :deep() 打到内层,
// 且不能写在 &__date 上——那样只命中外层,视觉上看不到变化。
&__date-wrap {
grid-column: 1 / -1;
display: flex;
justify-content: center;
width: 100%;
:deep(.case-filter__date),
:deep(.case-filter__date .el-range-editor) {
display: flex;
align-items: center;
width: 100%;
min-width: 0;
height: 53px;
padding: 0 @spacing-sm;
border-radius: @radius-base;
background: @input-bg;
box-sizing: border-box;
box-shadow: none;
justify-content: center;
}
}
&__date {
justify-content: center;
min-width: 0;
height: 53px;
padding: 0 @spacing-sm;
border-radius: @radius-base;
background: @input-bg;
box-sizing: border-box;
box-shadow: none;
&:hover,
@@ -469,6 +492,8 @@ function clearChips(): void {
}
:deep(.el-range-separator) {
flex: 0 0 auto;
margin: 0 @spacing-xs;
color: @cases-text;
font-size: 20px;
line-height: 53px;

View File

@@ -17,10 +17,6 @@ const emit = defineEmits<{
<template>
<div class="cases-grid">
<p class="cases-grid__total">
{{ total }}
</p>
<div v-if="cases.length > 0" class="cases-grid__list">
<CaseCard
v-for="item in cases"
@@ -39,17 +35,10 @@ const emit = defineEmits<{
<style lang="less" scoped>
.cases-grid {
&__total {
font-size: 20px;
font-weight: 700;
color: @heading-color;
}
&__list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 23px 28px;
margin-top: 12px;
}
&__empty {

View File

@@ -60,14 +60,18 @@ const keyword = computed<string>({
.case-search {
.flex(row, flex-start, center);
gap: 24px;
width: 100%;
height: 128px;
box-sizing: border-box;
padding: 0 36px;
// 设计稿 #31:输入框 x=653(栏左 606 + 47),重置 1305,搜索 1429
padding: 0 47px;
border-radius: @radius-base;
background: @white;
&__field {
position: relative;
// 输入框右边界 1245 → 重置按钮 1305,设计稿间距 60
margin-right: 36px;
flex: 0 0 592px;
}

View File

@@ -119,8 +119,12 @@ onMounted(() => {
background: @white;
}
// 设计稿 #31:hero 标题 x=112、副标题 x=112;
// 主体筛选栏 x=70、右侧操作区右边界 1850。两套留白并存,
// 所以 hero 内容单独内缩,不动全局 .container 的 70px 留白。
&__hero-inner {
padding-left: 42px;
padding-left: 112px;
padding-right: 112px;
}
&__title {
@@ -146,7 +150,10 @@ onMounted(() => {
}
&__main {
.flex(column, flex-start);
// 设计稿:右侧操作区 x=606 w=1244(右边界 1850,与筛选栏区留白 70 对称)。
// mixin 签名是 .flex(direction; justify; align),第三参才是 align-items。
// 纵向排列时 justify 无意义,align 用 stretch 让子项撑满 1244。
.flex(column, flex-start, stretch);
flex: 1;
min-width: 0;
gap: 24px;

View File

@@ -12,6 +12,10 @@ const router = useRouter()
const activeIndex = ref(0)
let timer: ReturnType<typeof setInterval> | null = null
// 后端未下发banner 时退回内置插图。插图在设计稿中并非铺满背景,
// 而是定位在右侧 (928,104) 尺寸 723x598,需按原比例显示。
const hasBanners = computed(() => props.banners.length > 0)
const slides = computed(() => {
if (props.banners.length > 0) {
return props.banners.map((item) => item.imageUrl)
@@ -64,7 +68,12 @@ onBeforeUnmount(stopAutoplay)
class="home-banner__slide"
:class="{ 'home-banner__slide--active': index === activeIndex }"
>
<img class="home-banner__image" :src="src" alt="">
<img
class="home-banner__image"
:class="{ 'home-banner__image--inline': !hasBanners }"
:src="src"
alt=""
>
</div>
</div>
@@ -128,6 +137,19 @@ onBeforeUnmount(stopAutoplay)
width: 100%;
height: 100%;
object-fit: cover;
// 内置插图:按设计稿定位与尺寸原样显示,不铺满
&--inline {
position: absolute;
top: 31px;
right: 269px;
left: auto;
bottom: auto;
width: 723px;
height: 598px;
margin-left: auto;
object-fit: contain;
}
}
&__inner {
@@ -216,25 +238,25 @@ onBeforeUnmount(stopAutoplay)
&__dots {
position: absolute;
right: 80px;
bottom: 40px;
left: 929px;
top: 594px;
z-index: 1;
.flex(row, flex-end, center);
gap: 8px;
.flex(row, flex-start, center);
gap: 13px;
}
&__dot {
width: 8px;
height: 8px;
width: 5px;
height: 5px;
padding: 0;
border: none;
border-radius: 50%;
background: fade(@home-banner-dot, 40%);
background: @home-banner-dot;
cursor: pointer;
&--active {
width: 24px;
border-radius: 4px;
width: 44px;
border-radius: 3px;
background: @home-banner-dot;
}
}

View File

@@ -1,10 +1,6 @@
<script setup lang="ts">
import type { BusinessType } from '@/types/api'
import type { BusinessLine } from '@/types/business/home'
import illInternet from '@/assets/home/business-internet.png'
import illHardware from '@/assets/home/business-hardware.png'
import illIot from '@/assets/home/business-iot.png'
import illManufacturing from '@/assets/home/business-manufacturing.png'
interface Props {
lines: BusinessLine[]
@@ -12,13 +8,7 @@ interface Props {
defineProps<Props>()
const FALLBACK_ILL: Record<BusinessType, string> = {
internet: illInternet,
hardware: illHardware,
iot: illIot,
manufacturing: illManufacturing,
other: illIot,
}
const FALLBACK_ACTIVE: BusinessType[] = ['internet']
</script>
<template>
@@ -31,13 +21,10 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
:key="line.id"
:to="`/business/${line.id}`"
class="home-business__card"
:class="`home-business__card--${line.id}`"
:class="{
'home-business__card--active': FALLBACK_ACTIVE.includes(line.id),
}"
>
<img
class="home-business__ill"
:src="line.icon || FALLBACK_ILL[line.id]"
:alt="line.name"
/>
<span class="home-business__check" aria-hidden="true" />
<h4 class="home-business__name">
{{ line.name }}
@@ -76,6 +63,7 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
min-width: 0;
height: 206px;
box-sizing: border-box;
padding: 0 20px;
overflow: hidden;
border: 1px solid @home-business-border;
border-radius: @radius-base;
@@ -90,19 +78,10 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
}
}
&__ill {
position: absolute;
top: 4px;
left: 48px;
width: 81px;
height: 111px;
object-fit: contain;
}
&__check {
position: absolute;
top: 24px;
right: 20px;
top: 27px;
right: 37px;
flex-shrink: 0;
width: 31px;
height: 31px;
@@ -113,8 +92,8 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
content: '';
position: absolute;
inset: 0;
width: 18px;
height: 18px;
width: 21px;
height: 21px;
margin: auto;
border-radius: @radius-round;
background: @white;
@@ -123,7 +102,7 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
}
&__name {
margin: 106px 0 0;
margin: 104px 0 0;
font-size: @font-size-title;
font-weight: 500;
line-height: 41px;
@@ -139,7 +118,7 @@ const FALLBACK_ILL: Record<BusinessType, string> = {
.multi-ellipsis(1);
}
&__card--internet {
&__card--active {
border-color: @primary-strong;
background: @primary-strong;

View File

@@ -173,13 +173,17 @@ function onMore(): void {
position: relative;
.flex(column, flex-start, stretch);
flex-shrink: 0;
flex: 0 1 722px;
flex: 0 0 529px;
width: 529px;
height: 245px;
box-sizing: border-box;
min-width: 0;
gap: 0;
padding: 18px 40px 28px;
border: 1px solid @home-card-border;
border-radius: @radius-lg;
background: @white;
overflow: hidden;
&__head {
.flex(row, flex-start);

View File

@@ -59,15 +59,17 @@ onMounted(() => {
&__dashboard {
.flex(row, space-between, stretch);
gap: 36px;
gap: 60px;
}
&__dashboard-main {
.flex(row, space-between, center);
flex: 1;
flex: 0 0 1091px;
width: 1091px;
min-width: 0;
box-sizing: border-box;
gap: @spacing-lg;
padding: 0 73px 0 106px;
padding: 0 59px 0 105px;
border: 1px solid @home-card-border;
border-radius: @radius-lg;
background: @white;

View File

@@ -177,7 +177,7 @@ async function onGetCode(target: string): Promise<void> {
right: 0;
bottom: 0;
left: 0;
height: calc(2px / @page-zoom);
height: 2px;
background: @primary-strong;
}
}