恢复修改的数据

This commit is contained in:
wangzzpeng
2026-09-28 10:47:49 +08:00
parent f1e64ef864
commit 43c2dc0d8d
57 changed files with 4557 additions and 1642 deletions

View File

@@ -16,24 +16,28 @@ defineProps<Props>()
</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"
:class="`home-business__card--${line.id}`"
>
<div class="home-business__icon">
<!-- TODO(蓝湖): 替换业务图标切图 -->
<span>{{ line.name.slice(0, 2) }}</span>
<div class="home-business__card-head">
<span class="home-business__icon">
<!-- TODO(蓝湖): 替换业务图标切图 -->
{{ line.name.slice(0, 2) }}
</span>
<h4 class="home-business__name">
{{ line.name }}
</h4>
</div>
<h4 class="home-business__name">
{{ line.name }}
</h4>
<p class="home-business__desc text-secondary">
<p class="home-business__desc">
{{ line.desc }}
</p>
<span class="home-business__more">了解详细 ></span>
<span class="home-business__more">了解详情 ></span>
</RouterLink>
</div>
</section>
@@ -64,29 +68,31 @@ defineProps<Props>()
&__card {
.flex(column, flex-start);
gap: @spacing-sm;
gap: @spacing-base;
padding: @spacing-lg;
border-radius: @radius-lg;
background: @white;
background: @primary-light;
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;
}
}
}
&__card-head {
.flex(row, flex-start);
gap: @spacing-sm;
}
&__icon {
.flex(row, center);
width: 48px;
height: 48px;
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: @radius-base;
background: @primary-light;
background: @white;
color: @primary-color;
font-size: @font-size-base;
font-weight: 600;
@@ -101,13 +107,68 @@ defineProps<Props>()
&__desc {
min-height: 40px;
font-size: @font-size-sm;
color: @text-color-secondary;
.multi-ellipsis(2);
}
&__more {
align-self: flex-end;
font-size: @font-size-sm;
color: @text-color-secondary;
transition: color 0.2s;
}
// 四类业务各自的主题色(与设计稿一致:卡片浅色底 + 同色系图标/文案)
&__card--internet {
background: #e8f0ff;
.home-business__icon {
color: #3370ff;
}
.home-business__desc,
.home-business__more {
color: #598bff;
}
}
&__card--hardware {
background: #e7f8ec;
.home-business__icon {
color: #22b14c;
}
.home-business__desc,
.home-business__more {
color: #34c724;
}
}
&__card--iot {
background: #f0eaff;
.home-business__icon {
color: #7c4dff;
}
.home-business__desc,
.home-business__more {
color: #9a7bff;
}
}
&__card--manufacturing {
background: #fff2e4;
.home-business__icon {
color: #ff8f1f;
}
.home-business__desc,
.home-business__more {
color: #ffa94d;
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { CaseCard } from '@/components'
import { formatBusinessType } from '@/utils/format'
import type { BusinessType } from '@/types/api'
import type { Case } from '@/types/business/cases'
@@ -46,16 +46,39 @@ function switchCategory(value: BusinessType | 'all'): void {
</button>
</div>
<RouterLink to="/cases" class="home-cases__more link">
查看更多 >
查看全部 >
</RouterLink>
</div>
<div class="home-cases__grid">
<CaseCard
<article
v-for="item in cases"
:key="item.id"
:item="item"
@view="$router.push(`/cases/${$event}`)"
/>
class="home-cases__card"
@click="$router.push(`/cases/${item.id}`)"
>
<div class="home-cases__cover">
<!-- TODO(蓝湖): 替换案例封面切图 -->
<img v-if="item.cover" v-lazy="item.cover" class="home-cases__img" :alt="item.title">
<div v-else class="home-cases__img home-cases__img--placeholder" />
<span class="home-cases__tag" :class="`home-cases__tag--${item.category}`">
{{ formatBusinessType(item.category) }}
</span>
</div>
<div class="home-cases__body">
<h4 class="home-cases__name">
{{ item.title }}
</h4>
<ul class="home-cases__meta">
<li>客户对象:{{ item.clientType }}</li>
<li>研发周期:{{ item.duration }}</li>
<li>团队规模:{{ item.teamSize }}人</li>
<li>完成时间:{{ item.finishDate }}</li>
</ul>
<span class="home-cases__view">查看详情</span>
</div>
</article>
</div>
</section>
</template>
@@ -64,6 +87,7 @@ function switchCategory(value: BusinessType | 'all'): void {
.home-cases {
&__head {
.flex(row, space-between);
gap: @spacing-base;
margin-bottom: @spacing-base;
}
@@ -75,6 +99,7 @@ function switchCategory(value: BusinessType | 'all'): void {
&__tabs {
.flex(row, center);
flex: 1;
gap: @spacing-sm;
}
@@ -102,5 +127,103 @@ function switchCategory(value: BusinessType | 'all'): void {
grid-template-columns: repeat(4, 1fr);
gap: @spacing-base;
}
&__card {
overflow: hidden;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
box-shadow: @shadow-lg;
transform: translateY(-2px);
.home-cases__view {
background: @primary-color;
color: @white;
}
}
}
&__cover {
position: relative;
height: 130px;
overflow: hidden;
}
&__img {
width: 100%;
height: 100%;
object-fit: cover;
&--placeholder {
background: linear-gradient(135deg, @primary-light, @bg-color);
}
}
&__tag {
position: absolute;
top: @spacing-sm;
left: @spacing-sm;
padding: 2px @spacing-sm;
border-radius: @radius-sm;
background: @primary-color;
color: @white;
font-size: @font-size-sm;
&--hardware {
background: #22b14c;
}
&--iot {
background: #7c4dff;
}
&--manufacturing {
background: #ff8f1f;
}
&--other {
background: @text-color-secondary;
}
}
&__body {
padding: @spacing-base;
}
&__name {
margin-bottom: @spacing-sm;
font-size: @font-size-lg;
font-weight: 600;
color: @heading-color;
.multi-ellipsis(1);
}
&__meta {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px @spacing-sm;
font-size: @font-size-sm;
color: @text-color-secondary;
li {
.ellipsis();
}
}
&__view {
display: block;
width: fit-content;
margin: @spacing-base 0 0 auto;
padding: 2px @spacing-base;
border: 1px solid @primary-color;
border-radius: @radius-sm;
color: @primary-color;
font-size: @font-size-sm;
transition: all 0.2s;
}
}
</style>

View File

@@ -1,70 +1,185 @@
<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: '📋' },
import { computed, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import { useHomeStore } from '@/stores/modules/home'
import type { PortalEntryCard } from '@/types/business/home'
import { toPortalEntryCards } from '@/utils/entryIcon'
import HomeAppDialog from './HomeAppDialog.vue'
/**
* 首页「快捷入口」卡片。
*
* 数据来自 store.quickEntries(← /vermins/portalEntryUser/list 的 groupType=1 项),
* 接口不可用(游客 / 无权限)时回退到内置默认入口,保证卡片不空。
* 「更多应用」打开 HomeAppDialog,可在快捷 / 其他两组之间调整入口。
*/
const homeStore = useHomeStore()
const { quickEntries } = storeToRefs(homeStore)
const router = useRouter()
const dialogVisible = ref(false)
/** 接口不可用时的默认入口(与改造前的静态入口保持一致) */
const FALLBACK_QUICK: PortalEntryCard[] = [
{ id: 0, label: '财务台账', route: '/finance/bills', groupType: '1', icon: '💰', isImage: false, tone: 'amber' },
{ id: 0, label: '合同台账', route: '/contract', groupType: '1', icon: '📄', isImage: false, tone: 'blue' },
{ id: 0, label: '客服工作台', route: '/service', groupType: '1', icon: '💬', isImage: false, tone: 'green' },
{ id: 0, label: '案例库管理', route: '/cases', groupType: '1', icon: '📁', isImage: false, tone: 'purple' },
{ id: 0, label: '工单需求池', route: '/demand', groupType: '1', icon: '📋', isImage: false, tone: 'cyan' },
]
/** 前端已注册的静态路由(route 无效时不跳转,避免出现无匹配路由) */
const knownPaths = new Set(router.getRoutes().map((item) => item.path))
/** 卡片展示的入口(最多 5 个) */
const entries = computed<PortalEntryCard[]>(() => {
const list = toPortalEntryCards(quickEntries.value)
return (list.length > 0 ? list : FALLBACK_QUICK).slice(0, 5)
})
/** 解析可跳转路径:优先后端 route,其次同名默认入口,最后回首页 */
function resolvePath(card: PortalEntryCard): string {
if (card.route && knownPaths.has(card.route)) return card.route
const fallback = FALLBACK_QUICK.find((item) => item.label === card.label)
if (fallback && knownPaths.has(fallback.route)) return fallback.route
return '/'
}
function onMore(): void {
// 打开时同步一次,确保分组是后端最新结果
void homeStore.fetchPortalEntries()
dialogVisible.value = true
}
</script>
<template>
<section class="home-quick">
<h3 class="home-quick__title">
快捷入口
</h3>
<div class="home-quick__head">
<span class="home-quick__head-icon">⊞</span>
<h3 class="home-quick__title">
快捷入口
</h3>
</div>
<div class="home-quick__list">
<RouterLink
v-for="entry in entries"
:key="entry.label"
:to="entry.path"
:key="`${entry.label}-${entry.id}`"
:to="resolvePath(entry)"
class="home-quick__item"
>
<span class="home-quick__icon">{{ entry.icon }}</span>
<span class="home-quick__icon" :class="`home-quick__icon--${entry.tone}`">
<img v-if="entry.isImage" :src="entry.icon" :alt="entry.label" class="home-quick__img">
<template v-else>{{ entry.icon }}</template>
</span>
<span class="home-quick__label">{{ entry.label }}</span>
</RouterLink>
</div>
<button class="home-quick__more link" type="button" @click="onMore">
更多应用 >
</button>
<HomeAppDialog v-model:visible="dialogVisible" />
</section>
</template>
<style lang="less" scoped>
.home-quick {
.flex(column, flex-start, stretch);
flex-shrink: 0;
width: 320px;
gap: @spacing-base;
padding: @spacing-lg;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
&__head {
.flex(row, flex-start);
gap: @spacing-sm;
}
&__head-icon {
color: @heading-color;
font-size: @font-size-lg;
}
&__title {
margin-bottom: @spacing-base;
font-size: @font-size-lg;
font-weight: 600;
color: @heading-color;
}
&__list {
.flex(row, space-between);
gap: @spacing-base;
.flex(row, space-between, flex-start);
gap: @spacing-sm;
}
&__item {
.flex(column, center);
.flex(column, flex-start, 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;
transition: transform 0.2s;
&:hover {
box-shadow: @shadow-lg;
transform: translateY(-2px);
}
}
&__icon {
font-size: 24px;
.flex(row, center);
width: 44px;
height: 44px;
border-radius: @radius-base;
font-size: @font-size-xl;
&--amber {
background: #fff3e0;
}
&--blue {
background: @primary-light;
}
&--green {
background: #e7f8ec;
}
&--purple {
background: #f0eaff;
}
&--red {
background: #ffeceb;
}
&--cyan {
background: #e4f6fa;
}
}
&__img {
width: 24px;
height: 24px;
object-fit: contain;
}
&__label {
font-size: @font-size-base;
font-size: @font-size-sm;
color: @text-color;
text-align: center;
}
&__more {
align-self: flex-end;
padding: 0;
border: 0;
background: transparent;
font-size: @font-size-sm;
color: @text-color-secondary;
cursor: pointer;
}
}
</style>

View File

@@ -7,10 +7,11 @@ interface Props {
defineProps<Props>()
/** 展示顺序与设计稿一致:累积项目 / 进行中 / 已提交 */
const items = [
{ key: 'pending', label: '待您处理' },
{ key: 'total', label: '累积项目' },
{ key: 'inProgress', label: '进行中' },
{ key: 'total', label: '累计项目' },
{ key: 'pending', label: '已提交' },
] as const
</script>
@@ -22,24 +23,23 @@ const items = [
class="home-stats__item"
>
<span class="home-stats__value">{{ stats[item.key] }}</span>
<span class="home-stats__label text-secondary">{{ item.label }}</span>
<span class="home-stats__label">{{ item.label }}</span>
</div>
</section>
</template>
<style lang="less" scoped>
.home-stats {
.flex(row, space-between);
.flex(row, flex-end);
gap: @spacing-base;
&__item {
.flex(column, center);
flex: 1;
.flex(column, center, center);
gap: @spacing-xs;
padding: @spacing-base @spacing-lg;
min-width: 84px;
padding: @spacing-base;
border-radius: @radius-lg;
background: @white;
box-shadow: @shadow-base;
background: rgba(255, 255, 255, 0.92);
}
&__value {
@@ -49,7 +49,8 @@ const items = [
}
&__label {
font-size: @font-size-base;
font-size: @font-size-sm;
color: @text-color-secondary;
}
}
</style>

View File

@@ -18,57 +18,42 @@ const greeting = computed(() => {
</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>
<div class="home-welcome">
<h2 class="home-welcome__greeting">
{{ greeting }}
<span class="home-welcome__wave">👋</span>
</h2>
<p class="home-welcome__summary">
{{ summary }}
</p>
</div>
</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;
.flex(column, center, flex-start);
gap: @spacing-sm;
&__greeting {
.flex(row, flex-start);
gap: @spacing-sm;
font-size: @font-size-xxl;
font-weight: 600;
color: @white;
}
&__summary {
margin-top: @spacing-xs;
color: rgba(255, 255, 255, 0.85);
}
&__brand {
&__wave {
.flex(row, center);
gap: @spacing-sm;
}
&__avatar {
.flex(row, center);
width: 40px;
height: 40px;
width: 32px;
height: 32px;
border-radius: @radius-round;
background: rgba(255, 255, 255, 0.25);
font-size: @font-size-lg;
}
&__appName {
font-size: @font-size-base;
&__summary {
font-size: @font-size-lg;
color: rgba(255, 255, 255, 0.9);
}
}
</style>

View File

@@ -1,12 +1,15 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import { AppButton } from '@/components'
// 业务详情页:数据只经 useHomeStore,不直接 import api
import { useHomeStore } from '@/stores/modules/home'
import type { BusinessType } from '@/types/api'
import HomeServiceTabs from './cpns/HomeServiceTabs.vue'
import HomeServiceContents from './cpns/HomeServiceContents.vue'
import HomeServiceTypes from './cpns/HomeServiceTypes.vue'
const route = useRoute()
const router = useRouter()
const homeStore = useHomeStore()
const line = computed(() => homeStore.currentBusiness)
@@ -47,10 +50,15 @@ onMounted(load)
</div>
</header>
<HomeServiceTabs
:service-types="line.serviceTypes"
:service-content="line.serviceContent"
/>
<HomeServiceTypes :service-types="line.serviceTypes" />
<HomeServiceContents :contents="line.contents" />
<div class="business-detail__cta">
<AppButton type="primary" size="lg" @click="router.push('/demand')">
立即提报
</AppButton>
</div>
</template>
<p v-else class="text-secondary">
@@ -104,5 +112,13 @@ onMounted(load)
&__slogan {
font-size: @font-size-base;
}
&__cta {
padding-top: @spacing-base;
.app-button {
width: 100%;
}
}
}
</style>

View File

@@ -1,7 +1,11 @@
export { default } from './index.vue'
export { default as HomeBanner } from './cpns/HomeBanner.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 HomeAppDialog } from './cpns/HomeAppDialog.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'
export { default as HomeServiceTypes } from './cpns/HomeServiceTypes.vue'
export { default as HomeServiceContents } from './cpns/HomeServiceContents.vue'

View File

@@ -4,23 +4,18 @@ import { storeToRefs } from 'pinia'
// 页面容器:只做组件引入、props 分发、事件中转,数据只经 store
import { useHomeStore } from '@/stores/modules/home'
import type { BusinessType } from '@/types/api'
import HomeBanner from './cpns/HomeBanner.vue'
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 { stats, banners, 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)
}
@@ -32,19 +27,28 @@ onMounted(() => {
<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 class="container">
<HomeBanner :banners="banners" />
</div>
<div class="container home__dashboard">
<section class="home__dashboard-main">
<HomeWelcome :summary="todaySummary" />
<HomeStats :stats="stats" />
</section>
<HomeQuickEntries />
</div>
<div class="container">
<HomeBusinessCards :lines="businessLines" />
</div>
<div class="container">
<HomeCaseShowcase
:cases="featuredCases"
@category-change="onCategoryChange"
/>
</div>
</div>
</template>
@@ -54,5 +58,20 @@ onMounted(() => {
gap: @spacing-xl;
padding-top: @spacing-xl;
padding-bottom: @spacing-xxl;
&__dashboard {
.flex(row, space-between, stretch);
gap: @spacing-base;
}
&__dashboard-main {
.flex(row, space-between, center);
flex: 1;
gap: @spacing-lg;
padding: @spacing-xl;
border-radius: @radius-lg;
background: linear-gradient(120deg, @primary-color, #6a5cff);
box-shadow: @shadow-base;
}
}
</style>