This commit is contained in:
wangzzpeng
2026-09-24 10:10:43 +08:00
commit 7bc84bbe5e
194 changed files with 22571 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
<script setup lang="ts">
import type { BusinessLine } from '@/types/business/home'
interface Props {
lines: BusinessLine[]
}
defineProps<Props>()
</script>
<template>
<section class="home-business">
<div class="home-business__head">
<h3 class="home-business__title">
业务服务
</h3>
<span class="home-business__slogan text-secondary">研发智造一体化自营平台,全链路质量兜底</span>
</div>
<div class="home-business__grid">
<RouterLink
v-for="line in lines"
:key="line.id"
:to="`/business/${line.id}`"
class="home-business__card"
>
<div class="home-business__icon">
<!-- TODO(蓝湖): 替换业务图标切图 -->
<span>{{ line.name.slice(0, 2) }}</span>
</div>
<h4 class="home-business__name">
{{ line.name }}
</h4>
<p class="home-business__desc text-secondary">
{{ line.desc }}
</p>
<span class="home-business__more">了解详细 ></span>
</RouterLink>
</div>
</section>
</template>
<style lang="less" scoped>
.home-business {
&__head {
.flex(row, space-between);
margin-bottom: @spacing-base;
}
&__title {
font-size: @font-size-xl;
font-weight: 600;
color: @heading-color;
}
&__slogan {
font-size: @font-size-sm;
}
&__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: @spacing-base;
}
&__card {
.flex(column, flex-start);
gap: @spacing-sm;
padding: @spacing-lg;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
box-shadow: @shadow-lg;
transform: translateY(-2px);
.home-business__more {
color: @primary-color;
}
}
}
&__icon {
.flex(row, center);
width: 48px;
height: 48px;
border-radius: @radius-base;
background: @primary-light;
color: @primary-color;
font-size: @font-size-base;
font-weight: 600;
}
&__name {
font-size: @font-size-lg;
font-weight: 600;
color: @heading-color;
}
&__desc {
min-height: 40px;
font-size: @font-size-sm;
.multi-ellipsis(2);
}
&__more {
font-size: @font-size-sm;
color: @text-color-secondary;
transition: color 0.2s;
}
}
</style>

View File

@@ -0,0 +1,106 @@
<script setup lang="ts">
import { ref } from 'vue'
import { CaseCard } from '@/components'
import type { BusinessType } from '@/types/api'
import type { Case } from '@/types/business/cases'
interface Props {
cases: Case[]
}
defineProps<Props>()
const emit = defineEmits<{ categoryChange: [category: BusinessType | 'all'] }>()
const activeCategory = ref<BusinessType | 'all'>('all')
const categories: Array<{ label: string; value: BusinessType | 'all' }> = [
{ label: '全部', value: 'all' },
{ label: '互联网软件', value: 'internet' },
{ label: '硬件电路', value: 'hardware' },
{ label: '物联网全案', value: 'iot' },
{ label: '生产制造', value: 'manufacturing' },
]
function switchCategory(value: BusinessType | 'all'): void {
activeCategory.value = value
emit('categoryChange', value)
}
</script>
<template>
<section class="home-cases">
<div class="home-cases__head">
<h3 class="home-cases__title">
案例库精选
</h3>
<div class="home-cases__tabs">
<button
v-for="cat in categories"
:key="cat.value"
class="home-cases__tab"
:class="{ 'is-active': activeCategory === cat.value }"
@click="switchCategory(cat.value)"
>
{{ cat.label }}
</button>
</div>
<RouterLink to="/cases" class="home-cases__more link">
查看更多 >
</RouterLink>
</div>
<div class="home-cases__grid">
<CaseCard
v-for="item in cases"
:key="item.id"
:item="item"
@view="$router.push(`/cases/${$event}`)"
/>
</div>
</section>
</template>
<style lang="less" scoped>
.home-cases {
&__head {
.flex(row, space-between);
margin-bottom: @spacing-base;
}
&__title {
font-size: @font-size-xl;
font-weight: 600;
color: @heading-color;
}
&__tabs {
.flex(row, center);
gap: @spacing-sm;
}
&__tab {
height: 30px;
padding: 0 @spacing-base;
border-radius: @radius-round;
background: @white;
color: @text-color;
font-size: @font-size-sm;
transition: all 0.2s;
&.is-active {
background: @primary-color;
color: @white;
}
}
&__more {
font-size: @font-size-base;
}
&__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: @spacing-base;
}
}
</style>

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
// 快捷入口为固定模块链接(纯 UI,静态数据)
const entries = [
{ label: '财务台账', path: '/finance/bills', icon: '💰' },
{ label: '合同台账', path: '/contract', icon: '📄' },
{ label: '客服工作台', path: '/service', icon: '💬' },
{ label: '案例库管理', path: '/cases', icon: '📁' },
{ label: '工单需求池', path: '/demand', icon: '📋' },
]
</script>
<template>
<section class="home-quick">
<h3 class="home-quick__title">
快捷入口
</h3>
<div class="home-quick__list">
<RouterLink
v-for="entry in entries"
:key="entry.label"
:to="entry.path"
class="home-quick__item"
>
<span class="home-quick__icon">{{ entry.icon }}</span>
<span class="home-quick__label">{{ entry.label }}</span>
</RouterLink>
</div>
</section>
</template>
<style lang="less" scoped>
.home-quick {
&__title {
margin-bottom: @spacing-base;
font-size: @font-size-lg;
font-weight: 600;
color: @heading-color;
}
&__list {
.flex(row, space-between);
gap: @spacing-base;
}
&__item {
.flex(column, center);
gap: @spacing-sm;
flex: 1;
padding: @spacing-base;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
box-shadow: @shadow-lg;
transform: translateY(-2px);
}
}
&__icon {
font-size: 24px;
}
&__label {
font-size: @font-size-base;
color: @text-color;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { ServiceType } from '@/types/business/home'
interface Props {
serviceTypes: ServiceType[]
serviceContent: string
}
defineProps<Props>()
const activeType = ref(0)
</script>
<template>
<section class="home-service">
<div class="home-service__head">
<h3 class="home-service__title">
服务内容
</h3>
<div class="home-service__types">
<button
v-for="(type, index) in serviceTypes"
:key="type.label"
class="home-service__type"
:class="{ 'is-active': activeType === index }"
@click="activeType = index"
>
{{ type.label }}
</button>
</div>
</div>
<p class="home-service__type-desc text-secondary">
{{ serviceTypes[activeType]?.desc }}
</p>
<div class="home-service__content">
<pre class="home-service__text">{{ serviceContent }}</pre>
<div class="home-service__video">
<!-- TODO(蓝湖): 接入真实视频源 -->
<span class="home-service__video-tip">视频将在预览时播放</span>
<span class="home-service__video-time">0:00 / 0:00</span>
</div>
</div>
</section>
</template>
<style lang="less" scoped>
.home-service {
padding: @spacing-lg @spacing-xl;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
&__head {
.flex(row, space-between);
margin-bottom: @spacing-sm;
}
&__title {
font-size: @font-size-xl;
font-weight: 600;
color: @heading-color;
}
&__types {
.flex(row, center);
gap: @spacing-sm;
}
&__type {
height: 30px;
padding: 0 @spacing-base;
border-radius: @radius-round;
background: @bg-color;
color: @text-color;
font-size: @font-size-sm;
transition: all 0.2s;
&.is-active {
background: @primary-color;
color: @white;
}
}
&__type-desc {
margin-bottom: @spacing-base;
font-size: @font-size-sm;
}
&__content {
.flex(row, flex-start);
gap: @spacing-lg;
}
&__text {
flex: 1;
margin: 0;
padding: @spacing-base;
border-radius: @radius-base;
background: @bg-color-light;
font-family: inherit;
font-size: @font-size-sm;
line-height: 1.8;
color: @text-color;
white-space: pre-wrap;
max-height: 320px;
overflow: auto;
.scrollbar();
}
&__video {
.flex(column, center);
gap: @spacing-sm;
width: 320px;
height: 200px;
border-radius: @radius-base;
background: @heading-color;
color: @text-color-secondary;
font-size: @font-size-sm;
}
&__video-time {
font-size: @font-size-sm;
color: @text-color-disabled;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import type { HomeStats } from '@/types/business/home'
interface Props {
stats: HomeStats
}
defineProps<Props>()
const items = [
{ key: 'pending', label: '待您处理' },
{ key: 'inProgress', label: '进行中' },
{ key: 'total', label: '累计项目' },
] as const
</script>
<template>
<section class="home-stats">
<div
v-for="item in items"
:key="item.key"
class="home-stats__item"
>
<span class="home-stats__value">{{ stats[item.key] }}</span>
<span class="home-stats__label text-secondary">{{ item.label }}</span>
</div>
</section>
</template>
<style lang="less" scoped>
.home-stats {
.flex(row, space-between);
gap: @spacing-base;
&__item {
.flex(column, center);
flex: 1;
gap: @spacing-xs;
padding: @spacing-base @spacing-lg;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
}
&__value {
font-size: @font-size-xxl;
font-weight: 600;
color: @primary-color;
}
&__label {
font-size: @font-size-base;
}
}
</style>

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
summary: string
}
defineProps<Props>()
const greeting = computed(() => {
const hour = new Date().getHours()
if (hour < 6) return '夜深了'
if (hour < 11) return '早上好'
if (hour < 14) return '中午好'
if (hour < 18) return '下午好'
return '晚上好'
})
</script>
<template>
<section class="home-welcome">
<div class="home-welcome__info">
<h2 class="home-welcome__greeting">
{{ greeting }}
</h2>
<p class="home-welcome__summary text-secondary">
{{ summary }}
</p>
</div>
<div class="home-welcome__brand">
<!-- TODO(蓝湖): 替换头像切图 -->
<span class="home-welcome__avatar">王</span>
<span class="home-welcome__appName">虫子智造</span>
</div>
</section>
</template>
<style lang="less" scoped>
.home-welcome {
.flex(row, space-between);
padding: @spacing-lg @spacing-xl;
border-radius: @radius-lg;
background: linear-gradient(120deg, @primary-color, @primary-hover);
color: @white;
&__greeting {
font-size: @font-size-xxl;
font-weight: 600;
}
&__summary {
margin-top: @spacing-xs;
color: rgba(255, 255, 255, 0.85);
}
&__brand {
.flex(row, center);
gap: @spacing-sm;
}
&__avatar {
.flex(row, center);
width: 40px;
height: 40px;
border-radius: @radius-round;
background: rgba(255, 255, 255, 0.25);
font-size: @font-size-lg;
}
&__appName {
font-size: @font-size-base;
}
}
</style>

108
src/views/home/detail.vue Normal file
View File

@@ -0,0 +1,108 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
// 业务详情页:数据只经 useHomeStore,不直接 import api
import { useHomeStore } from '@/stores/modules/home'
import type { BusinessType } from '@/types/api'
import HomeServiceTabs from './cpns/HomeServiceTabs.vue'
const route = useRoute()
const homeStore = useHomeStore()
const line = computed(() => homeStore.currentBusiness)
const businessType = computed(() => route.params.type as BusinessType)
function load(): void {
void homeStore.fetchBusinessDetail(businessType.value)
}
watch(businessType, load)
onMounted(load)
</script>
<template>
<div class="business-detail">
<nav class="business-detail__breadcrumb text-secondary">
<RouterLink to="/" class="link">
首页
</RouterLink>
<span> &gt; </span>
<span>业务详情</span>
</nav>
<template v-if="line">
<header class="business-detail__hero">
<span class="business-detail__logo">
<!-- TODO(蓝湖): 替换业务图标切图 -->
{{ line.name.slice(0, 2) }}
</span>
<div class="business-detail__intro">
<h1 class="business-detail__name">
{{ line.name }}
</h1>
<p class="business-detail__slogan text-secondary">
{{ line.slogan }}
</p>
</div>
</header>
<HomeServiceTabs
:service-types="line.serviceTypes"
:service-content="line.serviceContent"
/>
</template>
<p v-else class="text-secondary">
正在加载业务详情...
</p>
</div>
</template>
<style lang="less" scoped>
.business-detail {
padding-top: @spacing-lg;
padding-bottom: @spacing-xxl;
&__breadcrumb {
margin-bottom: @spacing-base;
font-size: @font-size-base;
}
&__hero {
.flex(row, flex-start);
gap: @spacing-base;
margin-bottom: @spacing-xl;
padding: @spacing-xl;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
}
&__logo {
.flex(row, center);
width: 56px;
height: 56px;
border-radius: @radius-lg;
background: @primary-light;
color: @primary-color;
font-size: @font-size-xl;
font-weight: 600;
}
&__intro {
.flex(column, flex-start);
gap: @spacing-xs;
}
&__name {
font-size: @font-size-xxl;
font-weight: 600;
color: @heading-color;
}
&__slogan {
font-size: @font-size-base;
}
}
</style>

7
src/views/home/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export { default } from './index.vue'
export { default as HomeWelcome } from './cpns/HomeWelcome.vue'
export { default as HomeStats } from './cpns/HomeStats.vue'
export { default as HomeQuickEntries } from './cpns/HomeQuickEntries.vue'
export { default as HomeBusinessCards } from './cpns/HomeBusinessCards.vue'
export { default as HomeCaseShowcase } from './cpns/HomeCaseShowcase.vue'
export { default as HomeServiceTabs } from './cpns/HomeServiceTabs.vue'

58
src/views/home/index.vue Normal file
View File

@@ -0,0 +1,58 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
// 页面容器:只做组件引入、props 分发、事件中转,数据只经 store
import { useHomeStore } from '@/stores/modules/home'
import type { BusinessType } from '@/types/api'
import HomeWelcome from './cpns/HomeWelcome.vue'
import HomeStats from './cpns/HomeStats.vue'
import HomeQuickEntries from './cpns/HomeQuickEntries.vue'
import HomeBusinessCards from './cpns/HomeBusinessCards.vue'
import HomeCaseShowcase from './cpns/HomeCaseShowcase.vue'
import HomeServiceTabs from './cpns/HomeServiceTabs.vue'
const homeStore = useHomeStore()
const { stats, businessLines, featuredCases } = storeToRefs(homeStore)
const todaySummary = computed(() => `今天有${stats.value.inProgress}个项目正在进行`)
// 服务 Tab 展示互联网软件线内容(服务内容区默认锚点)
const serviceLine = computed(
() => businessLines.value.find((line) => line.id === 'internet') ?? businessLines.value[0],
)
function onCategoryChange(category: BusinessType | 'all'): void {
void homeStore.fetchFeaturedCases(category)
}
onMounted(() => {
void homeStore.fetchHomeData()
})
</script>
<template>
<div class="home">
<HomeWelcome :summary="todaySummary" />
<HomeStats :stats="stats" />
<HomeQuickEntries />
<HomeBusinessCards :lines="businessLines" />
<HomeCaseShowcase
:cases="featuredCases"
@category-change="onCategoryChange"
/>
<HomeServiceTabs
v-if="serviceLine"
:service-types="serviceLine.serviceTypes"
:service-content="serviceLine.serviceContent"
/>
</div>
</template>
<style lang="less" scoped>
.home {
.flex(column, flex-start);
gap: @spacing-xl;
padding-top: @spacing-xl;
padding-bottom: @spacing-xxl;
}
</style>